ObjectStackObjectStack

Analytics schema — Data Protocol reference

Defines the "Business Logic" for data analysis. Inspired by Cube.dev, LookML, and dbt MetricFlow.

Analytics/Semantic Layer Protocol

Defines the "Business Logic" for data analysis. Inspired by Cube.dev, LookML, and dbt MetricFlow.

This layer decouples the "Physical Data" (Tables/Columns) from the "Business Data" (Metrics/Dimensions).

Source: packages/spec/src/data/analytics.zod.ts

TypeScript Usage

import { AggregationMetricType, AnalyticsDateRangeSchema, AnalyticsDateRangePresetSchema, AnalyticsQuerySchema, CubeSchema, CubeJoinSchema, DimensionSchema, DimensionType, MetricSchema, TimeUpdateInterval } from '@objectstack/spec/data';
import type { AggregationMetricType, AnalyticsDateRange, AnalyticsDateRangePreset, AnalyticsQuery, Cube, CubeJoin, Dimension, DimensionType, Metric, TimeUpdateInterval } from '@objectstack/spec/data';

// Validate data
const result = AggregationMetricType.parse(data);

AggregationMetricType

Allowed Values

  • count
  • sum
  • avg
  • min
  • max
  • count_distinct

AnalyticsDateRange

Union Options

This schema accepts one of the following structures:

Option 1

Allowed Values: today, yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days


Option 2

Type: [string, string]



AnalyticsDateRangePreset

Allowed Values

  • today
  • yesterday
  • this_week
  • last_week
  • this_month
  • last_month
  • this_quarter
  • last_quarter
  • this_year
  • last_year
  • last_7_days
  • last_30_days
  • last_90_days

AnalyticsQuery

Properties

PropertyTypeRequiredDescription
cubestringoptionalTarget cube name (optional when provided externally, e.g. in API request wrapper)
measuresstring[]✅List of metrics to calculate
dimensionsstring[]optionalList of dimensions to group by
whereanyoptionalFiltering criteria (canonical Query DSL FilterCondition). An authored FilterArray is lowered by parseFilterAST on the client before the wire; this field admits only the lowered FilterCondition (see FilterArray in data/filter.zod.ts).
timeDimensions{ dimension: string; granularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; dateRange?: Enum<'today' | 'yesterday' | 'this_week' | 'last_week' | 'this_month' | 'last_month' | …> | [string, string] }[]optionalTime-bucketed dimensions. Each entry names a dimension, an optional bucket granularity, and an optional dateRange — a preset name from the closed date-range vocabulary (e.g. 'last_7_days') or an explicit [start, end] window; an unrecognised string answers 400 ANALYTICS_DATE_RANGE_UNRECOGNIZED instead of silently widening.
orderRecord<string, Enum<'asc' | 'desc'>>optional
limitintegeroptionalMaximum number of rows to return, applied after order — a non-negative integer (0 returns no rows)
offsetintegeroptionalNumber of rows to skip before the first row returned, applied after order — a non-negative integer; an offset with no limit returns every row after it
timezonestringoptional

Nested Shape: AnalyticsQuery.timeDimensions[number]

PropertyTypeRequiredDescription
dimensionstring✅
granularityEnum<'day' | 'week' | 'month' | 'quarter' | 'year'>optional
dateRangeEnum<'today' | 'yesterday' | 'this_week' | 'last_week' | 'this_month' | 'last_month' | …> | [string, string]optionalTime window for this dimension: a date-range PRESET name from the closed vocabulary in data/date-range-presets.ts (today, yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days — e.g. 'last_7_days'), or an explicit [start, end] array of ISO dates / {date-macro} tokens (e.g. ["2023-01-01", "2023-01-31"]). Any other string is refused at the schema with 400 ANALYTICS_DATE_RANGE_UNRECOGNIZED.

Cube

Properties

PropertyTypeRequiredDescription
namestring✅Cube name (snake_case)
titlestringoptional
descriptionstringoptional
sqlstring✅Base SQL statement or Table Name
measuresRecord<string, { label: string; description?: string; type: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; sql: string; … }>✅Quantitative metrics, keyed by metric name: the record key IS the metric's name, published and queried as <cube>.<key>. A metric declares no inner name.
dimensionsRecord<string, { label: string; description?: string; type: Enum<'string' | 'number' | 'boolean' | 'time' | 'geo'>; sql: string; … }>✅Qualitative attributes, keyed by dimension name: the record key IS the dimension's name, published and queried as <cube>.<key>. A dimension declares no inner name.
joinsRecord<string, { name: string }>optional
refreshKeyneveroptional[REMOVED] analytics_cube.refreshKey was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing read it: no analytics result is cached, so neither every nor sql ever refreshed anything. Delete the key; every analytics query is computed when it is asked. A refresh cadence is declared again when a result cache exists. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
publicbooleanoptional (default: true)Whether the analytics API exposes this cube. Default true (visible). false hides it from GET /analytics/meta and refuses POST /analytics/query and /analytics/sql for it (CUBE_NOT_FOUND). Visibility only: the underlying object's permissions and row-level security still govern its records on every door.
_lockEnum<'none' | 'no-overlay' | 'no-delete' | 'full'>optionalItem-level lock — controls overlay & delete (ADR-0010).
_lockReasonstringoptionalHuman-readable reason shown when a write is refused by _lock.
_lockSourceEnum<'artifact' | 'package' | 'env-forced'>optionalLayer that set _lock (artifact | package | env-forced).
_provenanceEnum<'package' | 'org' | 'env-forced'>optionalOrigin of the item (package | org | env-forced).
_packageIdstringoptionalOwning package machine id.
_packageVersionstringoptionalOwning package version.
_lockDocsUrlstringoptionalOptional documentation link surfaced next to _lockReason.

Nested Shape: Cube.measures[string]

PropertyTypeRequiredDescription
nameneveroptional[REMOVED] measures.<metric>.name was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. Every consumer resolves a metric by its key in measures (GET /analytics/meta publishes it as <cube>.<key>, and a query names it that way), so the inner name was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a metric, rename its key in measures — and every query, dashboard and report that names <cube>.<key>. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
labelstring✅Human readable label
descriptionstringoptional
typeEnum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>✅
sqlstring✅Column reference: a field of the cube's object ("amount"), a relationship path ending in one ("account.amount"), or "*" for a count. Never a SQL expression: a derived value is declared on an ADR-0021 dataset (a measure-scoped filter, or derived: { op, of }).
formatstringoptionalDisplay format for this measure's result column: a numeral pattern such as "$0,0.00" or "0.0%". Relayed verbatim as fields[].format on POST /analytics/query results, and on the measure by GET /analytics/meta.

Nested Shape: Cube.dimensions[string]

PropertyTypeRequiredDescription
nameneveroptional[REMOVED] dimensions.<dimension>.name was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the dimension's name. Every consumer resolves a dimension by its key in dimensions (GET /analytics/meta publishes it as <cube>.<key>, and a query names it that way), so the inner name was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a dimension, rename its key in dimensions — and every query, dashboard and report that names <cube>.<key>. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
labelstring✅Human readable label
descriptionstringoptional
typeEnum<'string' | 'number' | 'boolean' | 'time' | 'geo'>✅
sqlstring✅Column reference: a field of the cube's object ("status") or a relationship path ending in one ("account.industry"). Never a SQL expression.
granularitiesEnum<'day' | 'week' | 'month' | 'quarter' | 'year'>[]optionalFor a time dimension. A single interval is its default bucket: a query that groups by this dimension without stating a granularity is bucketed at it. Two or more intervals state no default. A granularity the query states always wins, listed or not.

Nested Shape: Cube.joins[string]

PropertyTypeRequiredDescription
namestring✅Target cube name — the object this join reaches. The ON clause is DERIVED from the declared relationship between the two cubes' objects (a foreign-key equality) and is never authored. The KEY this join is declared under in the joins record is the FOREIGN-KEY FIELD on this cube's own object, not a second spelling of the object it reaches: the runtime emits LEFT JOIN <name> <key> ON <base>.<key> = <key>.id and resolves a member written <key>.<field> through that alias. A join keyed after the TARGET object joins on a column the base object does not have, so nothing resolves.

CubeJoin

Properties

PropertyTypeRequiredDescription
namestring✅Target cube name — the object this join reaches. The ON clause is DERIVED from the declared relationship between the two cubes' objects (a foreign-key equality) and is never authored. The KEY this join is declared under in the joins record is the FOREIGN-KEY FIELD on this cube's own object, not a second spelling of the object it reaches: the runtime emits LEFT JOIN <name> <key> ON <base>.<key> = <key>.id and resolves a member written <key>.<field> through that alias. A join keyed after the TARGET object joins on a column the base object does not have, so nothing resolves.

Dimension

Properties

PropertyTypeRequiredDescription
nameneveroptional[REMOVED] dimensions.<dimension>.name was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the dimension's name. Every consumer resolves a dimension by its key in dimensions (GET /analytics/meta publishes it as <cube>.<key>, and a query names it that way), so the inner name was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a dimension, rename its key in dimensions — and every query, dashboard and report that names <cube>.<key>. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
labelstring✅Human readable label
descriptionstringoptional
typeEnum<'string' | 'number' | 'boolean' | 'time' | 'geo'>✅
sqlstring✅Column reference: a field of the cube's object ("status") or a relationship path ending in one ("account.industry"). Never a SQL expression.
granularitiesEnum<'day' | 'week' | 'month' | 'quarter' | 'year'>[]optionalFor a time dimension. A single interval is its default bucket: a query that groups by this dimension without stating a granularity is bucketed at it. Two or more intervals state no default. A granularity the query states always wins, listed or not.

DimensionType

Allowed Values

  • string
  • number
  • boolean
  • time
  • geo

Metric

Properties

PropertyTypeRequiredDescription
nameneveroptional[REMOVED] measures.<metric>.name was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. Every consumer resolves a metric by its key in measures (GET /analytics/meta publishes it as <cube>.<key>, and a query names it that way), so the inner name was a second copy of the identity that nothing read, and one that disagreed with its key was silently ignored. Delete the key. To rename a metric, rename its key in measures — and every query, dashboard and report that names <cube>.<key>. Run os migrate meta --from 17 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
labelstring✅Human readable label
descriptionstringoptional
typeEnum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>✅
sqlstring✅Column reference: a field of the cube's object ("amount"), a relationship path ending in one ("account.amount"), or "*" for a count. Never a SQL expression: a derived value is declared on an ADR-0021 dataset (a measure-scoped filter, or derived: { op, of }).
formatstringoptionalDisplay format for this measure's result column: a numeral pattern such as "$0,0.00" or "0.0%". Relayed verbatim as fields[].format on POST /analytics/query results, and on the measure by GET /analytics/meta.

TimeUpdateInterval

Allowed Values

  • day
  • week
  • month
  • quarter
  • year

On this page