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. Resolve it into concrete [start, end] pairs for the window you actually render with the top-level helpers expandAttributeTimeIntervals, 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 do not mutate their input and perform no requests.

Where schedules live

The API returns schedules in two shapes, and both are accepted by the helpers:

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)

The one-call path for entity attributes: it walks the attribute's groups and schedules, expands each of them and merges the results. Merging matters — deduplication and ordering only hold within a single schedule, so combining groups by hand can yield duplicate or unsorted slots.

Anything that is not a timeInterval attribute yields an empty array, so it is safe to call without checking type first.

import { expandAttributeTimeIntervals } from 'oneentry';

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

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

expandTimeIntervals(schedule, window)

Resolves a single schedule. Reach for it when you already hold one — most notably on forms, whose schedules are already typed at localizeInfos.intervals:

import { expandTimeIntervals } from 'oneentry';

const form = await Forms.getFormByMarker('booking_form');
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, which is what lets you 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 at UTC day granularity — the time-of-day part of from/to is ignored.
  • The window is required: a schedule is an open-ended recurrence rule, and only you know how far it has to be resolved.

Recurrence semantics

  • dates[0] / range[0] is both the recurrence phase and the first valid day — nothing earlier is emitted, however wide the window.
  • dates[1] / range[1] ends validity. When it does not extend past the start, the schedule is anchored to that single day; with a recurrence flag set, recurrence is then open-ended and the window alone bounds the result.
  • inEveryWeek repeats every 7 days from the anchor.
  • inEveryMonth repeats on the same day-of-month, skipping months that are too short.
  • With both flags set, the weekly rule applies — which is what it has always meant in practice.
  • With neither flag the schedule is a plain date range: every day of it produces slots.
  • The result is deduplicated and sorted by start, then by end.
  • All arithmetic is UTC, so the result does not depend on the machine's timezone.

Migrating from the timeIntervals field

Earlier SDK versions injected a computed timeIntervals array into every timeInterval attribute value. That field no longer exists. It materialized 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 blow past framework data-cache limits. It was never declared in any interface or schema either, so TypeScript consumers could only reach it through a cast.

// before — 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 being expanded (dates/range, times/intervals, inEveryWeek, inEveryMonth) is unchanged and still present on every schedule — nothing is lost, it is simply resolved on demand instead of eagerly, 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 sharing 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;
}, {});