ObjectStackObjectStack

Analytics schema — API Protocol reference

Defines the HTTP interface for the Semantic Layer. Provides endpoints for executing analytical queries and discovering metadata.

Analytics API Protocol

Defines the HTTP interface for the Semantic Layer. Provides endpoints for executing analytical queries and discovering metadata.

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

TypeScript Usage

import { AnalyticsEndpoint, AnalyticsMetadataResponseSchema, AnalyticsQueryRequestSchema, AnalyticsResultResponseSchema, AnalyticsSqlResponseSchema, DatasetCompareToSchema, DatasetSelectionSchema, DatasetTotalsSchema, GetAnalyticsMetaRequestSchema } from '@objectstack/spec/api';
import type { AnalyticsEndpoint, AnalyticsMetadataResponse, AnalyticsQueryRequest, AnalyticsResultResponse, AnalyticsSqlResponse, DatasetCompareTo, DatasetSelection, DatasetTotals, GetAnalyticsMetaRequest } from '@objectstack/spec/api';

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

AnalyticsEndpoint

Allowed Values

  • /api/v1/analytics/query
  • /api/v1/analytics/meta
  • /api/v1/analytics/sql

AnalyticsMetadataResponse

Properties

PropertyTypeRequiredDescription
successboolean✅Operation success status
error{ code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }optionalError details if success is false
meta{ timestamp: string; duration?: integer; requestId?: string; traceId?: string }optionalResponse metadata
data{ name: string; title?: string; description?: string; measures: object[]; … }[]✅Available cubes, each as the CubeMeta discovery projection — the cube name, its title and description, and the measures/dimensions a client may name in a query. A bare array: there is no cubes wrapper object, and no cube sql is published.

Nested Shape: AnalyticsMetadataResponse.error

PropertyTypeRequiredDescription
codeEnum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>✅Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages)
declaredCodestringoptionalThe producer-declared code, verbatim, when it is not a member of the closed code vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112)
messagestring✅Readable error message
userMessagestringoptionalProducer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces message.
refusaltrueoptionalProducer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
categorystringoptionalError category (e.g. validation, authorization)
httpStatusintegeroptionalHTTP status of the response carrying this error
detailsanyoptionalAdditional error context (e.g. field validation errors)
requestIdstringoptionalRequest ID for tracking

Nested Shape: AnalyticsMetadataResponse.meta

PropertyTypeRequiredDescription
timestampstring✅
durationintegeroptionalServer-side processing duration in milliseconds
requestIdstringoptional
traceIdstringoptional

Nested Shape: AnalyticsMetadataResponse.data[number]

PropertyTypeRequiredDescription
namestring✅Cube name
titlestringoptionalHuman-readable cube title
descriptionstringoptionalCube description, projected from the cube definition's description
measures{ name: string; type: string; title?: string; description?: string; … }[]✅Measures this cube accepts in /analytics/query
dimensions{ name: string; type: string; title?: string; description?: string }[]✅Dimensions this cube accepts in /analytics/query

AnalyticsQueryRequest

Properties

PropertyTypeRequiredDescription
cubestring✅Target cube name
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
queryneveroptional[REMOVED] query was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0. The { cube, query: {...} } envelope was the dialect of the retired degraded analytics shim — the real engine never understood it. Move the query.* fields to the body top level: { cube, measures, dimensions?, where?, timeDimensions?, order?, limit?, offset?, timezone? }.
formatneveroptional[REMOVED] format was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0. It was never implemented — every response is the JSON envelope. Delete the key; for CSV/XLSX use the export surface instead.

Nested Shape: AnalyticsQueryRequest.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.

AnalyticsResultResponse

Properties

PropertyTypeRequiredDescription
successboolean✅Operation success status
error{ code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }optionalError details if success is false
meta{ timestamp: string; duration?: integer; requestId?: string; traceId?: string }optionalResponse metadata
data{ rows: Record<string, any>[]; fields: object[]; sql?: string; totals?: object[]; … }✅

Nested Shape: AnalyticsResultResponse.error

PropertyTypeRequiredDescription
codeEnum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>✅Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages)
declaredCodestringoptionalThe producer-declared code, verbatim, when it is not a member of the closed code vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112)
messagestring✅Readable error message
userMessagestringoptionalProducer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces message.
refusaltrueoptionalProducer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
categorystringoptionalError category (e.g. validation, authorization)
httpStatusintegeroptionalHTTP status of the response carrying this error
detailsanyoptionalAdditional error context (e.g. field validation errors)
requestIdstringoptionalRequest ID for tracking

Nested Shape: AnalyticsResultResponse.meta

PropertyTypeRequiredDescription
timestampstring✅
durationintegeroptionalServer-side processing duration in milliseconds
requestIdstringoptional
traceIdstringoptional

Nested Shape: AnalyticsResultResponse.data

PropertyTypeRequiredDescription
rowsRecord<string, any>[]✅Result rows
fields{ name: string; type: string; label?: string; format?: string; … }[]✅Column metadata
sqlstringoptionalExecuted SQL (if debug enabled)
totals{ dimensions: string[]; rows: Record<string, any>[] }[]optionalMarginal aggregates - one entry per requested totals grouping, in request order, each computed with the measure's true aggregate over the underlying data (never re-derived from bucketed values). The grand-total grouping yields a single dimensionless row.
objectstringoptionalThe base object of the dataset the answer was computed from: the dataset's object, by machine name. Every dataset answer (POST /analytics/dataset/query) must carry it, whatever dimensions are selected and whether or not rows came back, so a consumer can refresh on that object's record changes and drill into its records. Absent on a cube query answer, which has no dataset behind it.
dimensionFieldsRecord<string, string>optionalDrill-through sidecar: each equality-drillable dimension the answer is grouped by, dimension name to that dataset dimension's own field (a base-object field or a relationship.field path). Set only on a dataset answer (POST /analytics/dataset/query) that has rows and groups by at least one dimension with a field that is not type: 'date'; a date dimension is never listed (see drillRanges). Absent on a cube query answer.
drillRawRowsRecord<string, any>[]optionalDrill-through sidecar, aligned to rows by index: each dimensionFields dimension name to the stored value that row was grouped by (a select option's value, a lookup's record id), captured before label resolution rewrites the row to its display label. An exact-match drill filter is built from these, not from the labels in rows. Set exactly when dimensionFields is.
drillRawTotalsRecord<string, any>[][]optionalDrill-through sidecar for totals: entry [i][j] aligns to totals[i].rows[j] and holds the stored value of the dimensionFields dimensions that grouping groups by, so the grand-total grouping yields an empty map per row. Set only when dimensionFields is and the answer carries at least one totals grouping.
drillRangesRecord<string, { field: string; gte: string; lt: string }>[]optionalDrill-through sidecar for time buckets, aligned to rows by index: date dimension name to the half-open range [gte, lt) that row's bucket covers. Bounds are YYYY-MM-DD calendar days, or ISO-8601 instants at midnight in the reference timezone when the source field is datetime; a row whose bucket value is null gets no entry for that dimension. Set only on a dataset answer that has rows and groups by at least one type: 'date' dimension with a field and a granularity; independent of dimensionFields. Absent on a cube query answer.

AnalyticsSqlResponse

Properties

PropertyTypeRequiredDescription
successboolean✅Operation success status
error{ code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }optionalError details if success is false
meta{ timestamp: string; duration?: integer; requestId?: string; traceId?: string }optionalResponse metadata
data{ sql: string; params: any[] }✅

Nested Shape: AnalyticsSqlResponse.error

PropertyTypeRequiredDescription
codeEnum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>✅Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages)
declaredCodestringoptionalThe producer-declared code, verbatim, when it is not a member of the closed code vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112)
messagestring✅Readable error message
userMessagestringoptionalProducer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces message.
refusaltrueoptionalProducer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
categorystringoptionalError category (e.g. validation, authorization)
httpStatusintegeroptionalHTTP status of the response carrying this error
detailsanyoptionalAdditional error context (e.g. field validation errors)
requestIdstringoptionalRequest ID for tracking

Nested Shape: AnalyticsSqlResponse.meta

PropertyTypeRequiredDescription
timestampstring✅
durationintegeroptionalServer-side processing duration in milliseconds
requestIdstringoptional
traceIdstringoptional

DatasetCompareTo

Properties

PropertyTypeRequiredDescription
kindEnum<'previousPeriod' | 'previousYear'>✅Comparison window: previousPeriod (equal-length, immediately before) or previousYear (the same window one calendar year back)
dimensionstringoptionalTime dimension to shift; omit when the selection has exactly one dated time dimension

DatasetSelection

Properties

PropertyTypeRequiredDescription
dimensionsstring[]optionalList of dimensions to group by
measuresstring[]✅List of metrics to calculate
runtimeFilteranyoptionalPresentation-scope filter (canonical Query DSL FilterCondition), ANDed with the dataset's intrinsic filter at render
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.
dateGranularityEnum<'day' | 'week' | 'month' | 'quarter' | 'year'>optionalPresentation-scope date bucketing applied to every selected date dimension; an explicit timeDimensions entry wins over it, and the dataset dimension's own default is used when neither is set
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
compareTo{ kind: Enum<'previousPeriod' | 'previousYear'>; dimension?: string }optionalPeriod-over-period comparison window ({ kind, dimension? }); attaches <measure>__compare columns
totals{ groupings: string[][] }optionalServer-side marginal aggregates; each grouping is a dimension subset to additionally aggregate by, [] being the grand total
timezonestringoptional

Nested Shape: DatasetSelection.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.

Nested Shape: DatasetSelection.compareTo

PropertyTypeRequiredDescription
kindEnum<'previousPeriod' | 'previousYear'>✅Comparison window: previousPeriod (equal-length, immediately before) or previousYear (the same window one calendar year back)
dimensionstringoptionalTime dimension to shift; omit when the selection has exactly one dated time dimension

Nested Shape: DatasetSelection.totals

PropertyTypeRequiredDescription
groupingsstring[][]✅Dimension subsets to additionally aggregate by, in request order; the empty subset is the grand total

DatasetTotals

Properties

PropertyTypeRequiredDescription
groupingsstring[][]✅Dimension subsets to additionally aggregate by, in request order; the empty subset is the grand total

GetAnalyticsMetaRequest

Properties

PropertyTypeRequiredDescription
cubestringoptionalOptional cube name to filter

On this page