Skip to main content

Time intervals

A timeInterval attribute is a recurrence rule, not a list of slots: an anchor date, daily time ranges and repeat flags. To turn it into concrete [start, end] pairs for the window you render, use the top-level helpers expandAttributeTimeIntervals and expandTimeIntervals and the type guard isTimeIntervalAttribute.

import { expandAttributeTimeIntervals } from 'oneentry';

const slots = expandAttributeTimeIntervals(page.attributeValues.interval, {
from: '2025-04-01',
to: '2025-04-30',
});
// [['2025-04-14T09:00:00.000Z', '2025-04-14T10:00:00.000Z'], …]

All three helpers are pure: they don't mutate their input and make no requests.

Where schedules live​

The API returns schedules in two shapes, and the helpers accept both:

ShapeWhere to find itCarries
Entity (ITimeIntervalEntitySchedule)attributeValues[marker].value[].values[] on pages, products, blocks and attribute setsa dates range with times pairs
Form (ITimeIntervalSchedule)attributes[].localizeInfos.intervals[] on formsa range with intervals that carry a slot period in minutes

expandAttributeTimeIntervals(attr, window)​

Expands an entity attribute in one call: it walks the attribute's groups and schedules, expands each one and merges the results. The merge matters, because deduplication and ordering only hold within a single schedule; combining groups by hand can give you duplicate or unsorted slots.

For anything that isn't a timeInterval attribute it returns an empty array, so you can call it without checking type first.

import { expandAttributeTimeIntervals } from 'oneentry';

const page = await Pages.getPageByUrl('booking');
if ('statusCode' in page) throw new Error(page.message);

const slots = expandAttributeTimeIntervals(page.attributeValues.interval, {
from: '2025-04-01',
to: '2025-04-30',
});

expandTimeIntervals(schedule, window)​

Expands a single schedule. Use it when you already have one, typically on forms, whose schedules are already typed at localizeInfos.intervals:

import { expandTimeIntervals } from 'oneentry';

const form = await Forms.getFormByMarker('booking_form');
if ('statusCode' in form) throw new Error(form.message);
const field = form.attributes.find((a) => a.marker === 'schedule');

const slots = (field?.localizeInfos.intervals ?? []).flatMap((schedule) =>
expandTimeIntervals(schedule, { from: '2025-05-01', to: '2025-05-31' }),
);

isTimeIntervalAttribute(attr)​

IAttributeValue.value is typed unknown, because its shape depends on type. This type guard narrows an attribute to ITimeIntervalAttributeValue, so you can reach the schedules without a cast:

import { isTimeIntervalAttribute } from 'oneentry';

const attr = page.attributeValues.interval;

if (isTimeIntervalAttribute(attr)) {
attr.value[0].values[0].dates; // fully typed
}

The window​

const window = { from: '2025-04-01', to: '2025-04-30' };
  • from and to accept a Date, an ISO string or epoch milliseconds.
  • Both bounds are inclusive and compared by UTC day; the time of day in from/to is ignored.
  • The window is required: a schedule is an open-ended recurrence rule, and only you know how far to expand it.

Recurrence semantics​

  • dates[0] / range[0] is both the recurrence phase and the first valid day. Nothing earlier is returned, however wide the window.
  • dates[1] / range[1] ends validity. If it isn't later than the start, the schedule is anchored to that single day; with a recurrence flag set, the recurrence is then open-ended and only the window bounds the result.
  • inEveryWeek repeats every 7 days from the anchor.
  • inEveryMonth repeats on the same day of the month, skipping months that are too short.
  • With both flags set, the weekly rule applies.
  • With neither flag, the schedule is a plain date range: every day in it produces slots.
  • The result is deduplicated and sorted by start, then by end.
  • All arithmetic is in UTC, so the result doesn't depend on the machine's timezone.

Migrating from the timeIntervals field​

Earlier SDK versions added a computed timeIntervals array to every timeInterval attribute value. That field no longer exists. It held a full year of slots regardless of what the caller needed: a single attribute with hourly slots expanded to roughly 2,000 lines of JSON, and finer slot periods reached megabytes, enough to exceed framework data-cache limits. It was never declared in any interface or schema either, so TypeScript code could only reach it through a cast.

Before, you read the pre-computed field:

const slots = page.attributeValues.interval.value[0].values[0].timeIntervals;

Now, expand the window you actually render:

import { expandAttributeTimeIntervals } from 'oneentry';

const slots = expandAttributeTimeIntervals(page.attributeValues.interval, {
from: '2025-04-01',
to: '2025-04-30',
});

The source data (dates/range, times/intervals, inEveryWeek, inEveryMonth) is unchanged and still present on every schedule. Nothing is lost: slots are computed on demand instead of up front, and the compact rule is what gets cached.

The _addTimeIntervalsToSchedules and _addTimeIntervalsToFormSchedules methods were removed from every module as well (despite the _ prefix they were callable, e.g. Pages._addTimeIntervalsToSchedules). Use expandTimeIntervals instead.

Types​

All of these are exported from the package root and from oneentry/types (see Importing types):

TypeDescribes
ITimeIntervalAttributeValueAn IAttributeValue narrowed to type: 'timeInterval', whose value is an array of groups
ITimeIntervalGroupOne entry of the attribute's value: schedules that share an intervalId
ITimeIntervalEntityScheduleOne entity schedule: dates, times, inEveryWeek, inEveryMonth
ITimeIntervalScheduleOne form schedule: range, intervals, inEveryWeek, inEveryMonth
ITimeIntervalRangeA daily range with start, end and a slot period in minutes (null when not sliced)
ITimeIntervalPointA point in a day: { hours, minutes }
ITimeIntervalWindowThe expansion window: { from, to }
TimeIntervalPairOne resolved slot: [start, end], both ISO 8601 UTC strings

Example: rendering a month of slots​

import { expandAttributeTimeIntervals } from 'oneentry';

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

if ('statusCode' in page) {
throw new Error(page.message);
}

const slots = expandAttributeTimeIntervals(page.attributeValues.interval, {
from: '2025-04-01',
to: '2025-04-30',
});

// Group the slots by day for a calendar view
const byDay = slots.reduce((acc, [start, end]) => {
const day = start.slice(0, 10);
(acc[day] ??= []).push([start, end]);
return acc;
}, {} as Record<string, Array<[string, string]>>);