Skip to content

Commit ad4ae2a

Browse files
committed
feat(analytics): add analytics protocol schemas for metrics, dimensions, and queries
1 parent 4f0d1ae commit ad4ae2a

File tree

2 files changed

+170
-1
lines changed

2 files changed

+170
-1
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { z } from 'zod';
2+
import { FilterConditionSchema } from './filter.zod';
3+
4+
/**
5+
* Analytics/Semantic Layer Protocol
6+
*
7+
* Defines the "Business Logic" for data analysis.
8+
* Inspired by Cube.dev, LookML, and dbt MetricFlow.
9+
*
10+
* This layer decouples the "Physical Data" (Tables/Columns) from the
11+
* "Business Data" (Metrics/Dimensions).
12+
*/
13+
14+
/**
15+
* Metric Type
16+
* The mathematical operation to perform.
17+
*/
18+
export const MetricType = z.enum([
19+
'count',
20+
'sum',
21+
'avg',
22+
'min',
23+
'max',
24+
'count_distinct',
25+
'number', // Custom SQL expression returning a number
26+
'string', // Custom SQL expression returning a string
27+
'boolean' // Custom SQL expression returning a boolean
28+
]);
29+
30+
/**
31+
* Dimension Type
32+
* The nature of the grouping field.
33+
*/
34+
export const DimensionType = z.enum([
35+
'string',
36+
'number',
37+
'boolean',
38+
'time',
39+
'geo'
40+
]);
41+
42+
/**
43+
* Time Interval for Time Dimensions
44+
*/
45+
export const TimeUpdateInterval = z.enum([
46+
'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'
47+
]);
48+
49+
/**
50+
* Metric Schema
51+
* A quantitative measurement (e.g., "Total Revenue", "Average Order Value").
52+
*/
53+
export const MetricSchema = z.object({
54+
name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Unique metric ID'),
55+
label: z.string().describe('Human readable label'),
56+
description: z.string().optional(),
57+
58+
type: MetricType,
59+
60+
/** Source Calculation */
61+
sql: z.string().describe('SQL expression or field reference'),
62+
63+
/** Filtering for this specific metric (e.g. "Revenue from Premium Users") */
64+
filters: z.array(z.object({
65+
sql: z.string()
66+
})).optional(),
67+
68+
/** Format for display (e.g. "currency", "percent") */
69+
format: z.string().optional(),
70+
});
71+
72+
/**
73+
* Dimension Schema
74+
* A categorical attribute to group by (e.g., "Product Category", "Order Date").
75+
*/
76+
export const DimensionSchema = z.object({
77+
name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Unique dimension ID'),
78+
label: z.string().describe('Human readable label'),
79+
description: z.string().optional(),
80+
81+
type: DimensionType,
82+
83+
/** Source Column */
84+
sql: z.string().describe('SQL expression or column reference'),
85+
86+
/** For Time Dimensions: Supported Granularities */
87+
granularities: z.array(TimeUpdateInterval).optional(),
88+
});
89+
90+
/**
91+
* Join Schema
92+
* Defines how this cube relates to others.
93+
*/
94+
export const CubeJoinSchema = z.object({
95+
name: z.string().describe('Target cube name'),
96+
relationship: z.enum(['one_to_one', 'one_to_many', 'many_to_one']).default('many_to_one'),
97+
sql: z.string().describe('Join condition (ON clause)'),
98+
});
99+
100+
/**
101+
* Cube Schema
102+
* A logical data model representing a business entity or process for analysis.
103+
* Maps physical tables to business metrics and dimensions.
104+
*/
105+
export const CubeSchema = z.object({
106+
name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Cube name (snake_case)'),
107+
title: z.string().optional(),
108+
description: z.string().optional(),
109+
110+
/** Physical Data Source */
111+
sql: z.string().describe('Base SQL statement or Table Name'),
112+
113+
/** Semantic Definitions */
114+
measures: z.record(z.string(), MetricSchema).describe('Quantitative metrics'),
115+
dimensions: z.record(z.string(), DimensionSchema).describe('Qualitative attributes'),
116+
117+
/** Relationships */
118+
joins: z.record(z.string(), CubeJoinSchema).optional(),
119+
120+
/** Pre-aggregations / Caching */
121+
refreshKey: z.object({
122+
every: z.string().optional(), // e.g. "1 hour"
123+
sql: z.string().optional(), // SQL to check for data changes
124+
}).optional(),
125+
126+
/** Access Control */
127+
public: z.boolean().default(false),
128+
});
129+
130+
/**
131+
* Analytics Query Schema
132+
* The request format for the Analytics API.
133+
*/
134+
export const AnalyticsQuerySchema = z.object({
135+
measures: z.array(z.string()).describe('List of metrics to calculate'),
136+
dimensions: z.array(z.string()).optional().describe('List of dimensions to group by'),
137+
138+
filters: z.array(z.object({
139+
member: z.string().describe('Dimension or Measure'),
140+
operator: z.enum(['equals', 'notEquals', 'contains', 'notContains', 'gt', 'gte', 'lt', 'lte', 'set', 'notSet', 'inDateRange']),
141+
values: z.array(z.string()).optional(),
142+
})).optional(),
143+
144+
timeDimensions: z.array(z.object({
145+
dimension: z.string(),
146+
granularity: TimeUpdateInterval.optional(),
147+
dateRange: z.union([
148+
z.string(), // "Last 7 days"
149+
z.array(z.string()) // ["2023-01-01", "2023-01-31"]
150+
]).optional(),
151+
})).optional(),
152+
153+
order: z.record(z.string(), z.enum(['asc', 'desc'])).optional(),
154+
155+
limit: z.number().optional(),
156+
offset: z.number().optional(),
157+
158+
timezone: z.string().default('UTC'),
159+
});
160+
161+
export type Metric = z.infer<typeof MetricSchema>;
162+
export type Dimension = z.infer<typeof DimensionSchema>;
163+
export type Cube = z.infer<typeof CubeSchema>;
164+
export type AnalyticsQuery = z.infer<typeof AnalyticsQuerySchema>;

packages/spec/src/data/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ export * from './dataset.zod';
1616
export * from './document.zod';
1717

1818
// External Lookup Protocol
19-
export * from './external-lookup.zod'; export * from './datasource.zod';
19+
export * from './external-lookup.zod';
20+
export * from './datasource.zod';
21+
22+
// Analytics Protocol (Semantic Layer)
23+
export * from './analytics.zod';
24+

0 commit comments

Comments
 (0)