business-as-code 0.2.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +17 -0
- package/IMPLEMENTATION.md +226 -0
- package/README.md +1133 -193
- package/dist/business.d.ts +62 -0
- package/dist/business.d.ts.map +1 -0
- package/dist/business.js +109 -0
- package/dist/business.js.map +1 -0
- package/dist/dollar.d.ts +60 -0
- package/dist/dollar.d.ts.map +1 -0
- package/dist/dollar.js +107 -0
- package/dist/dollar.js.map +1 -0
- package/dist/entities/assets.d.ts +21 -0
- package/dist/entities/assets.d.ts.map +1 -0
- package/dist/entities/assets.js +323 -0
- package/dist/entities/assets.js.map +1 -0
- package/dist/entities/business.d.ts +36 -0
- package/dist/entities/business.d.ts.map +1 -0
- package/dist/entities/business.js +370 -0
- package/dist/entities/business.js.map +1 -0
- package/dist/entities/communication.d.ts +21 -0
- package/dist/entities/communication.d.ts.map +1 -0
- package/dist/entities/communication.js +255 -0
- package/dist/entities/communication.js.map +1 -0
- package/dist/entities/customers.d.ts +58 -0
- package/dist/entities/customers.d.ts.map +1 -0
- package/dist/entities/customers.js +989 -0
- package/dist/entities/customers.js.map +1 -0
- package/dist/entities/financials.d.ts +59 -0
- package/dist/entities/financials.d.ts.map +1 -0
- package/dist/entities/financials.js +932 -0
- package/dist/entities/financials.js.map +1 -0
- package/dist/entities/goals.d.ts +58 -0
- package/dist/entities/goals.d.ts.map +1 -0
- package/dist/entities/goals.js +800 -0
- package/dist/entities/goals.js.map +1 -0
- package/dist/entities/index.d.ts +299 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +198 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/legal.d.ts +21 -0
- package/dist/entities/legal.d.ts.map +1 -0
- package/dist/entities/legal.js +301 -0
- package/dist/entities/legal.js.map +1 -0
- package/dist/entities/market.d.ts +21 -0
- package/dist/entities/market.d.ts.map +1 -0
- package/dist/entities/market.js +301 -0
- package/dist/entities/market.js.map +1 -0
- package/dist/entities/marketing.d.ts +67 -0
- package/dist/entities/marketing.d.ts.map +1 -0
- package/dist/entities/marketing.js +1157 -0
- package/dist/entities/marketing.js.map +1 -0
- package/dist/entities/offerings.d.ts +51 -0
- package/dist/entities/offerings.d.ts.map +1 -0
- package/dist/entities/offerings.js +727 -0
- package/dist/entities/offerings.js.map +1 -0
- package/dist/entities/operations.d.ts +58 -0
- package/dist/entities/operations.d.ts.map +1 -0
- package/dist/entities/operations.js +787 -0
- package/dist/entities/operations.js.map +1 -0
- package/dist/entities/organization.d.ts +57 -0
- package/dist/entities/organization.d.ts.map +1 -0
- package/dist/entities/organization.js +807 -0
- package/dist/entities/organization.js.map +1 -0
- package/dist/entities/partnerships.d.ts +21 -0
- package/dist/entities/partnerships.d.ts.map +1 -0
- package/dist/entities/partnerships.js +300 -0
- package/dist/entities/partnerships.js.map +1 -0
- package/dist/entities/planning.d.ts +87 -0
- package/dist/entities/planning.d.ts.map +1 -0
- package/dist/entities/planning.js +271 -0
- package/dist/entities/planning.js.map +1 -0
- package/dist/entities/projects.d.ts +25 -0
- package/dist/entities/projects.d.ts.map +1 -0
- package/dist/entities/projects.js +349 -0
- package/dist/entities/projects.js.map +1 -0
- package/dist/entities/risk.d.ts +21 -0
- package/dist/entities/risk.d.ts.map +1 -0
- package/dist/entities/risk.js +293 -0
- package/dist/entities/risk.js.map +1 -0
- package/dist/entities/sales.d.ts +72 -0
- package/dist/entities/sales.d.ts.map +1 -0
- package/dist/entities/sales.js +1248 -0
- package/dist/entities/sales.js.map +1 -0
- package/dist/financials.d.ts +130 -0
- package/dist/financials.d.ts.map +1 -0
- package/dist/financials.js +297 -0
- package/dist/financials.js.map +1 -0
- package/dist/goals.d.ts +87 -0
- package/dist/goals.d.ts.map +1 -0
- package/dist/goals.js +215 -0
- package/dist/goals.js.map +1 -0
- package/dist/index.d.ts +97 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +131 -1079
- package/dist/index.js.map +1 -1
- package/dist/kpis.d.ts +118 -0
- package/dist/kpis.d.ts.map +1 -0
- package/dist/kpis.js +232 -0
- package/dist/kpis.js.map +1 -0
- package/dist/metrics.d.ts +448 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +325 -0
- package/dist/metrics.js.map +1 -0
- package/dist/okrs.d.ts +123 -0
- package/dist/okrs.d.ts.map +1 -0
- package/dist/okrs.js +269 -0
- package/dist/okrs.js.map +1 -0
- package/dist/organization.d.ts +585 -0
- package/dist/organization.d.ts.map +1 -0
- package/dist/organization.js +173 -0
- package/dist/organization.js.map +1 -0
- package/dist/process.d.ts +112 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +241 -0
- package/dist/process.js.map +1 -0
- package/dist/product.d.ts +85 -0
- package/dist/product.d.ts.map +1 -0
- package/dist/product.js +145 -0
- package/dist/product.js.map +1 -0
- package/dist/queries.d.ts +304 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +415 -0
- package/dist/queries.js.map +1 -0
- package/dist/roles.d.ts +340 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +255 -0
- package/dist/roles.js.map +1 -0
- package/dist/service.d.ts +61 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +140 -0
- package/dist/service.js.map +1 -0
- package/dist/types.d.ts +459 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/vision.d.ts +38 -0
- package/dist/vision.d.ts.map +1 -0
- package/dist/vision.js +68 -0
- package/dist/vision.js.map +1 -0
- package/dist/workflow.d.ts +115 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +247 -0
- package/dist/workflow.js.map +1 -0
- package/examples/basic-usage.ts +307 -0
- package/package.json +19 -60
- package/src/business.ts +121 -0
- package/src/dollar.ts +132 -0
- package/src/entities/assets.ts +332 -0
- package/src/entities/business.ts +406 -0
- package/src/entities/communication.ts +264 -0
- package/src/entities/customers.ts +1072 -0
- package/src/entities/financials.ts +1011 -0
- package/src/entities/goals.ts +871 -0
- package/src/entities/index.ts +383 -0
- package/src/entities/legal.ts +310 -0
- package/src/entities/market.ts +310 -0
- package/src/entities/marketing.ts +1249 -0
- package/src/entities/offerings.ts +789 -0
- package/src/entities/operations.ts +861 -0
- package/src/entities/organization.ts +876 -0
- package/src/entities/partnerships.ts +309 -0
- package/src/entities/planning.ts +307 -0
- package/src/entities/projects.ts +360 -0
- package/src/entities/risk.ts +302 -0
- package/src/entities/sales.ts +1352 -0
- package/src/financials.ts +352 -0
- package/src/goals.ts +250 -0
- package/src/index.test.ts +336 -0
- package/src/index.ts +530 -0
- package/src/kpis.ts +275 -0
- package/src/metrics.ts +825 -0
- package/src/okrs.ts +325 -0
- package/src/organization.ts +909 -0
- package/src/process.ts +272 -0
- package/src/product.ts +178 -0
- package/src/queries.ts +767 -0
- package/src/roles.ts +686 -0
- package/src/service.ts +164 -0
- package/src/types.ts +493 -0
- package/src/vision.ts +88 -0
- package/src/workflow.ts +280 -0
- package/tsconfig.json +9 -0
- package/dist/loaders/index.d.ts +0 -174
- package/dist/loaders/index.js +0 -366
- package/dist/loaders/index.js.map +0 -1
- package/dist/schema/index.d.ts +0 -146
- package/dist/schema/index.js +0 -716
- package/dist/schema/index.js.map +0 -1
- package/dist/types-CJ9eGS_C.d.ts +0 -86
package/src/queries.ts
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Queries & Views
|
|
3
|
+
*
|
|
4
|
+
* Query definitions for real-time analytics against ai-database (ClickHouse-backed).
|
|
5
|
+
* These are NOT batch reports - they're live, composable queries that execute
|
|
6
|
+
* in real-time against a performant OLAP database.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TimePeriod } from './types.js'
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Core Query Types
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Time granularity for aggregations
|
|
19
|
+
*/
|
|
20
|
+
export type Granularity =
|
|
21
|
+
| 'minute'
|
|
22
|
+
| 'hour'
|
|
23
|
+
| 'day'
|
|
24
|
+
| 'week'
|
|
25
|
+
| 'month'
|
|
26
|
+
| 'quarter'
|
|
27
|
+
| 'year'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Aggregation function
|
|
31
|
+
*/
|
|
32
|
+
export type AggregateFunction =
|
|
33
|
+
| 'sum'
|
|
34
|
+
| 'avg'
|
|
35
|
+
| 'min'
|
|
36
|
+
| 'max'
|
|
37
|
+
| 'count'
|
|
38
|
+
| 'countDistinct'
|
|
39
|
+
| 'first'
|
|
40
|
+
| 'last'
|
|
41
|
+
| 'median'
|
|
42
|
+
| 'p50'
|
|
43
|
+
| 'p90'
|
|
44
|
+
| 'p95'
|
|
45
|
+
| 'p99'
|
|
46
|
+
| 'stddev'
|
|
47
|
+
| 'variance'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Comparison operator
|
|
51
|
+
*/
|
|
52
|
+
export type Operator =
|
|
53
|
+
| 'eq' // =
|
|
54
|
+
| 'ne' // !=
|
|
55
|
+
| 'gt' // >
|
|
56
|
+
| 'gte' // >=
|
|
57
|
+
| 'lt' // <
|
|
58
|
+
| 'lte' // <=
|
|
59
|
+
| 'in' // IN
|
|
60
|
+
| 'notIn' // NOT IN
|
|
61
|
+
| 'like' // LIKE
|
|
62
|
+
| 'notLike' // NOT LIKE
|
|
63
|
+
| 'between' // BETWEEN
|
|
64
|
+
| 'isNull' // IS NULL
|
|
65
|
+
| 'isNotNull' // IS NOT NULL
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sort direction
|
|
69
|
+
*/
|
|
70
|
+
export type SortDirection = 'asc' | 'desc'
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Dimension & Measure (OLAP Primitives)
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Dimension - categorical attribute for grouping/filtering
|
|
78
|
+
*/
|
|
79
|
+
export interface Dimension {
|
|
80
|
+
name: string
|
|
81
|
+
field: string // Source field in database
|
|
82
|
+
type: 'string' | 'number' | 'date' | 'boolean'
|
|
83
|
+
description?: string
|
|
84
|
+
granularity?: Granularity // For time dimensions
|
|
85
|
+
format?: string // Display format
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Measure - numeric value to aggregate
|
|
90
|
+
*/
|
|
91
|
+
export interface Measure {
|
|
92
|
+
name: string
|
|
93
|
+
field: string // Source field or expression
|
|
94
|
+
aggregate: AggregateFunction
|
|
95
|
+
type?: 'number' | 'currency' | 'percent'
|
|
96
|
+
description?: string
|
|
97
|
+
format?: string
|
|
98
|
+
currency?: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Calculated measure - derived from other measures
|
|
103
|
+
*/
|
|
104
|
+
export interface CalculatedMeasure {
|
|
105
|
+
name: string
|
|
106
|
+
expression: string // e.g., "revenue - cogs" or "revenue / customers"
|
|
107
|
+
measures: string[] // Dependencies
|
|
108
|
+
type?: 'number' | 'currency' | 'percent'
|
|
109
|
+
description?: string
|
|
110
|
+
format?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// =============================================================================
|
|
114
|
+
// Filter & Sort
|
|
115
|
+
// =============================================================================
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Filter condition
|
|
119
|
+
*/
|
|
120
|
+
export interface Filter {
|
|
121
|
+
field: string
|
|
122
|
+
operator: Operator
|
|
123
|
+
value: unknown
|
|
124
|
+
and?: Filter[]
|
|
125
|
+
or?: Filter[]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sort specification
|
|
130
|
+
*/
|
|
131
|
+
export interface Sort {
|
|
132
|
+
field: string
|
|
133
|
+
direction: SortDirection
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Time range filter
|
|
138
|
+
*/
|
|
139
|
+
export interface TimeRange {
|
|
140
|
+
field: string // The timestamp field
|
|
141
|
+
start?: Date | string // Absolute or relative (e.g., '-30d')
|
|
142
|
+
end?: Date | string
|
|
143
|
+
granularity?: Granularity
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// =============================================================================
|
|
147
|
+
// Query Definition
|
|
148
|
+
// =============================================================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Query definition - a composable, reusable query
|
|
152
|
+
*/
|
|
153
|
+
export interface Query {
|
|
154
|
+
name: string
|
|
155
|
+
description?: string
|
|
156
|
+
|
|
157
|
+
// Data source
|
|
158
|
+
source: string // Table or view name
|
|
159
|
+
|
|
160
|
+
// What to select
|
|
161
|
+
dimensions?: string[] // Dimension names to group by
|
|
162
|
+
measures?: string[] // Measure names to aggregate
|
|
163
|
+
|
|
164
|
+
// Filtering
|
|
165
|
+
filters?: Filter[]
|
|
166
|
+
timeRange?: TimeRange
|
|
167
|
+
|
|
168
|
+
// Sorting & pagination
|
|
169
|
+
sort?: Sort[]
|
|
170
|
+
limit?: number
|
|
171
|
+
offset?: number
|
|
172
|
+
|
|
173
|
+
// Metadata
|
|
174
|
+
tags?: string[]
|
|
175
|
+
owner?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Query with resolved schema
|
|
180
|
+
*/
|
|
181
|
+
export interface ResolvedQuery extends Query {
|
|
182
|
+
resolvedDimensions: Dimension[]
|
|
183
|
+
resolvedMeasures: (Measure | CalculatedMeasure)[]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// View Definition (Like Materialized Views)
|
|
188
|
+
// =============================================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* View - a saved query with optional materialization
|
|
192
|
+
*/
|
|
193
|
+
export interface View {
|
|
194
|
+
name: string
|
|
195
|
+
description?: string
|
|
196
|
+
query: Query
|
|
197
|
+
|
|
198
|
+
// Materialization options
|
|
199
|
+
materialized?: boolean
|
|
200
|
+
refreshInterval?: string // e.g., '5m', '1h', '1d'
|
|
201
|
+
retention?: string // How long to keep data
|
|
202
|
+
|
|
203
|
+
// Access
|
|
204
|
+
public?: boolean
|
|
205
|
+
owner?: string
|
|
206
|
+
tags?: string[]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Dashboard - collection of related views
|
|
211
|
+
*/
|
|
212
|
+
export interface Dashboard {
|
|
213
|
+
name: string
|
|
214
|
+
description?: string
|
|
215
|
+
views: View[]
|
|
216
|
+
layout?: DashboardLayout
|
|
217
|
+
refreshInterval?: string
|
|
218
|
+
owner?: string
|
|
219
|
+
tags?: string[]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Dashboard layout
|
|
224
|
+
*/
|
|
225
|
+
export interface DashboardLayout {
|
|
226
|
+
columns: number
|
|
227
|
+
rows: number
|
|
228
|
+
items: DashboardItem[]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Dashboard item position
|
|
233
|
+
*/
|
|
234
|
+
export interface DashboardItem {
|
|
235
|
+
viewName: string
|
|
236
|
+
x: number
|
|
237
|
+
y: number
|
|
238
|
+
width: number
|
|
239
|
+
height: number
|
|
240
|
+
visualization?: Visualization
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Visualization type
|
|
245
|
+
*/
|
|
246
|
+
export type Visualization =
|
|
247
|
+
| 'number' // Single big number
|
|
248
|
+
| 'trend' // Number with sparkline
|
|
249
|
+
| 'table' // Data table
|
|
250
|
+
| 'bar' // Bar chart
|
|
251
|
+
| 'line' // Line chart
|
|
252
|
+
| 'area' // Area chart
|
|
253
|
+
| 'pie' // Pie chart
|
|
254
|
+
| 'funnel' // Funnel chart
|
|
255
|
+
| 'cohort' // Cohort matrix
|
|
256
|
+
| 'heatmap' // Heatmap
|
|
257
|
+
|
|
258
|
+
// =============================================================================
|
|
259
|
+
// Metric Definitions (Standard SaaS Metrics as Queries)
|
|
260
|
+
// =============================================================================
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Standard SaaS metric dimensions
|
|
264
|
+
*/
|
|
265
|
+
export const StandardDimensions: Record<string, Dimension> = {
|
|
266
|
+
// Time
|
|
267
|
+
date: { name: 'date', field: 'date', type: 'date', description: 'Event date' },
|
|
268
|
+
month: { name: 'month', field: 'date', type: 'date', granularity: 'month', description: 'Month' },
|
|
269
|
+
quarter: { name: 'quarter', field: 'date', type: 'date', granularity: 'quarter', description: 'Quarter' },
|
|
270
|
+
year: { name: 'year', field: 'date', type: 'date', granularity: 'year', description: 'Year' },
|
|
271
|
+
|
|
272
|
+
// Customer
|
|
273
|
+
customerId: { name: 'customerId', field: 'customer_id', type: 'string', description: 'Customer ID' },
|
|
274
|
+
customerSegment: { name: 'customerSegment', field: 'customer_segment', type: 'string', description: 'Customer segment' },
|
|
275
|
+
plan: { name: 'plan', field: 'plan', type: 'string', description: 'Subscription plan' },
|
|
276
|
+
cohort: { name: 'cohort', field: 'cohort', type: 'string', description: 'Customer cohort' },
|
|
277
|
+
|
|
278
|
+
// Product
|
|
279
|
+
productId: { name: 'productId', field: 'product_id', type: 'string', description: 'Product ID' },
|
|
280
|
+
productName: { name: 'productName', field: 'product_name', type: 'string', description: 'Product name' },
|
|
281
|
+
feature: { name: 'feature', field: 'feature', type: 'string', description: 'Feature name' },
|
|
282
|
+
|
|
283
|
+
// Geography
|
|
284
|
+
country: { name: 'country', field: 'country', type: 'string', description: 'Country' },
|
|
285
|
+
region: { name: 'region', field: 'region', type: 'string', description: 'Region' },
|
|
286
|
+
|
|
287
|
+
// Channel
|
|
288
|
+
channel: { name: 'channel', field: 'channel', type: 'string', description: 'Acquisition channel' },
|
|
289
|
+
source: { name: 'source', field: 'source', type: 'string', description: 'Traffic source' },
|
|
290
|
+
campaign: { name: 'campaign', field: 'campaign', type: 'string', description: 'Marketing campaign' },
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Standard SaaS metric measures
|
|
295
|
+
*/
|
|
296
|
+
export const StandardMeasures: Record<string, Measure> = {
|
|
297
|
+
// Revenue
|
|
298
|
+
revenue: { name: 'revenue', field: 'revenue', aggregate: 'sum', type: 'currency', description: 'Total revenue' },
|
|
299
|
+
mrr: { name: 'mrr', field: 'mrr', aggregate: 'sum', type: 'currency', description: 'Monthly recurring revenue' },
|
|
300
|
+
newMrr: { name: 'newMrr', field: 'new_mrr', aggregate: 'sum', type: 'currency', description: 'New MRR' },
|
|
301
|
+
expansionMrr: { name: 'expansionMrr', field: 'expansion_mrr', aggregate: 'sum', type: 'currency', description: 'Expansion MRR' },
|
|
302
|
+
contractionMrr: { name: 'contractionMrr', field: 'contraction_mrr', aggregate: 'sum', type: 'currency', description: 'Contraction MRR' },
|
|
303
|
+
churnedMrr: { name: 'churnedMrr', field: 'churned_mrr', aggregate: 'sum', type: 'currency', description: 'Churned MRR' },
|
|
304
|
+
|
|
305
|
+
// Customers
|
|
306
|
+
customers: { name: 'customers', field: 'customer_id', aggregate: 'countDistinct', type: 'number', description: 'Unique customers' },
|
|
307
|
+
newCustomers: { name: 'newCustomers', field: 'new_customer_id', aggregate: 'countDistinct', type: 'number', description: 'New customers' },
|
|
308
|
+
churnedCustomers: { name: 'churnedCustomers', field: 'churned_customer_id', aggregate: 'countDistinct', type: 'number', description: 'Churned customers' },
|
|
309
|
+
|
|
310
|
+
// Usage
|
|
311
|
+
events: { name: 'events', field: 'event_id', aggregate: 'count', type: 'number', description: 'Event count' },
|
|
312
|
+
sessions: { name: 'sessions', field: 'session_id', aggregate: 'countDistinct', type: 'number', description: 'Unique sessions' },
|
|
313
|
+
activeUsers: { name: 'activeUsers', field: 'user_id', aggregate: 'countDistinct', type: 'number', description: 'Active users' },
|
|
314
|
+
|
|
315
|
+
// Costs
|
|
316
|
+
cogs: { name: 'cogs', field: 'cogs', aggregate: 'sum', type: 'currency', description: 'Cost of goods sold' },
|
|
317
|
+
salesSpend: { name: 'salesSpend', field: 'sales_spend', aggregate: 'sum', type: 'currency', description: 'Sales spend' },
|
|
318
|
+
marketingSpend: { name: 'marketingSpend', field: 'marketing_spend', aggregate: 'sum', type: 'currency', description: 'Marketing spend' },
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Calculated SaaS metrics
|
|
323
|
+
*/
|
|
324
|
+
export const CalculatedMetrics: Record<string, CalculatedMeasure> = {
|
|
325
|
+
// Revenue metrics
|
|
326
|
+
arr: {
|
|
327
|
+
name: 'arr',
|
|
328
|
+
expression: 'mrr * 12',
|
|
329
|
+
measures: ['mrr'],
|
|
330
|
+
type: 'currency',
|
|
331
|
+
description: 'Annual recurring revenue',
|
|
332
|
+
},
|
|
333
|
+
netNewMrr: {
|
|
334
|
+
name: 'netNewMrr',
|
|
335
|
+
expression: 'newMrr + expansionMrr - contractionMrr - churnedMrr',
|
|
336
|
+
measures: ['newMrr', 'expansionMrr', 'contractionMrr', 'churnedMrr'],
|
|
337
|
+
type: 'currency',
|
|
338
|
+
description: 'Net new MRR',
|
|
339
|
+
},
|
|
340
|
+
arpu: {
|
|
341
|
+
name: 'arpu',
|
|
342
|
+
expression: 'mrr / customers',
|
|
343
|
+
measures: ['mrr', 'customers'],
|
|
344
|
+
type: 'currency',
|
|
345
|
+
description: 'Average revenue per user',
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
// Margin metrics
|
|
349
|
+
grossProfit: {
|
|
350
|
+
name: 'grossProfit',
|
|
351
|
+
expression: 'revenue - cogs',
|
|
352
|
+
measures: ['revenue', 'cogs'],
|
|
353
|
+
type: 'currency',
|
|
354
|
+
description: 'Gross profit',
|
|
355
|
+
},
|
|
356
|
+
grossMargin: {
|
|
357
|
+
name: 'grossMargin',
|
|
358
|
+
expression: '(revenue - cogs) / revenue * 100',
|
|
359
|
+
measures: ['revenue', 'cogs'],
|
|
360
|
+
type: 'percent',
|
|
361
|
+
description: 'Gross margin percentage',
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
// Efficiency metrics
|
|
365
|
+
cac: {
|
|
366
|
+
name: 'cac',
|
|
367
|
+
expression: '(salesSpend + marketingSpend) / newCustomers',
|
|
368
|
+
measures: ['salesSpend', 'marketingSpend', 'newCustomers'],
|
|
369
|
+
type: 'currency',
|
|
370
|
+
description: 'Customer acquisition cost',
|
|
371
|
+
},
|
|
372
|
+
ltv: {
|
|
373
|
+
name: 'ltv',
|
|
374
|
+
expression: 'arpu * grossMargin / 100 / churnRate',
|
|
375
|
+
measures: ['arpu', 'grossMargin'],
|
|
376
|
+
type: 'currency',
|
|
377
|
+
description: 'Customer lifetime value',
|
|
378
|
+
},
|
|
379
|
+
ltvCacRatio: {
|
|
380
|
+
name: 'ltvCacRatio',
|
|
381
|
+
expression: 'ltv / cac',
|
|
382
|
+
measures: ['ltv', 'cac'],
|
|
383
|
+
type: 'number',
|
|
384
|
+
description: 'LTV:CAC ratio',
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
// Churn metrics
|
|
388
|
+
customerChurnRate: {
|
|
389
|
+
name: 'customerChurnRate',
|
|
390
|
+
expression: 'churnedCustomers / customers * 100',
|
|
391
|
+
measures: ['churnedCustomers', 'customers'],
|
|
392
|
+
type: 'percent',
|
|
393
|
+
description: 'Customer churn rate',
|
|
394
|
+
},
|
|
395
|
+
revenueChurnRate: {
|
|
396
|
+
name: 'revenueChurnRate',
|
|
397
|
+
expression: 'churnedMrr / mrr * 100',
|
|
398
|
+
measures: ['churnedMrr', 'mrr'],
|
|
399
|
+
type: 'percent',
|
|
400
|
+
description: 'Revenue churn rate',
|
|
401
|
+
},
|
|
402
|
+
nrr: {
|
|
403
|
+
name: 'nrr',
|
|
404
|
+
expression: '(mrr + expansionMrr - contractionMrr - churnedMrr) / mrr * 100',
|
|
405
|
+
measures: ['mrr', 'expansionMrr', 'contractionMrr', 'churnedMrr'],
|
|
406
|
+
type: 'percent',
|
|
407
|
+
description: 'Net revenue retention',
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
// Growth metrics
|
|
411
|
+
quickRatio: {
|
|
412
|
+
name: 'quickRatio',
|
|
413
|
+
expression: '(newMrr + expansionMrr) / (contractionMrr + churnedMrr)',
|
|
414
|
+
measures: ['newMrr', 'expansionMrr', 'contractionMrr', 'churnedMrr'],
|
|
415
|
+
type: 'number',
|
|
416
|
+
description: 'SaaS Quick Ratio',
|
|
417
|
+
},
|
|
418
|
+
magicNumber: {
|
|
419
|
+
name: 'magicNumber',
|
|
420
|
+
expression: 'netNewMrr * 12 / (salesSpend + marketingSpend)',
|
|
421
|
+
measures: ['netNewMrr', 'salesSpend', 'marketingSpend'],
|
|
422
|
+
type: 'number',
|
|
423
|
+
description: 'Magic Number',
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// =============================================================================
|
|
428
|
+
// Query Builder Functions
|
|
429
|
+
// =============================================================================
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Create a query
|
|
433
|
+
*/
|
|
434
|
+
export function query(name: string, source: string): QueryBuilder {
|
|
435
|
+
return new QueryBuilder(name, source)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Fluent query builder
|
|
440
|
+
*/
|
|
441
|
+
export class QueryBuilder {
|
|
442
|
+
private _query: Query
|
|
443
|
+
|
|
444
|
+
constructor(name: string, source: string) {
|
|
445
|
+
this._query = { name, source }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
describe(description: string): this {
|
|
449
|
+
this._query.description = description
|
|
450
|
+
return this
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
dimensions(...dims: string[]): this {
|
|
454
|
+
this._query.dimensions = dims
|
|
455
|
+
return this
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
measures(...measures: string[]): this {
|
|
459
|
+
this._query.measures = measures
|
|
460
|
+
return this
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
filter(field: string, operator: Operator, value: unknown): this {
|
|
464
|
+
if (!this._query.filters) this._query.filters = []
|
|
465
|
+
this._query.filters.push({ field, operator, value })
|
|
466
|
+
return this
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
where(filters: Filter[]): this {
|
|
470
|
+
this._query.filters = filters
|
|
471
|
+
return this
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
timeRange(field: string, start?: Date | string, end?: Date | string, granularity?: Granularity): this {
|
|
475
|
+
this._query.timeRange = { field, start, end, granularity }
|
|
476
|
+
return this
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
last(duration: string, field: string = 'date'): this {
|
|
480
|
+
this._query.timeRange = { field, start: `-${duration}` }
|
|
481
|
+
return this
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
sort(field: string, direction: SortDirection = 'desc'): this {
|
|
485
|
+
if (!this._query.sort) this._query.sort = []
|
|
486
|
+
this._query.sort.push({ field, direction })
|
|
487
|
+
return this
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
limit(n: number): this {
|
|
491
|
+
this._query.limit = n
|
|
492
|
+
return this
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
offset(n: number): this {
|
|
496
|
+
this._query.offset = n
|
|
497
|
+
return this
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
tags(...tags: string[]): this {
|
|
501
|
+
this._query.tags = tags
|
|
502
|
+
return this
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
owner(owner: string): this {
|
|
506
|
+
this._query.owner = owner
|
|
507
|
+
return this
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
build(): Query {
|
|
511
|
+
return { ...this._query }
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// =============================================================================
|
|
516
|
+
// Pre-built SaaS Metric Queries
|
|
517
|
+
// =============================================================================
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* MRR Overview query
|
|
521
|
+
*/
|
|
522
|
+
export const MrrOverview = query('mrr_overview', 'revenue_events')
|
|
523
|
+
.describe('Monthly recurring revenue breakdown')
|
|
524
|
+
.dimensions('month')
|
|
525
|
+
.measures('mrr', 'newMrr', 'expansionMrr', 'contractionMrr', 'churnedMrr', 'netNewMrr')
|
|
526
|
+
.last('12m')
|
|
527
|
+
.sort('month', 'asc')
|
|
528
|
+
.build()
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* ARR by segment query
|
|
532
|
+
*/
|
|
533
|
+
export const ArrBySegment = query('arr_by_segment', 'revenue_events')
|
|
534
|
+
.describe('Annual recurring revenue by customer segment')
|
|
535
|
+
.dimensions('customerSegment')
|
|
536
|
+
.measures('arr', 'customers', 'arpu')
|
|
537
|
+
.last('1m')
|
|
538
|
+
.sort('arr', 'desc')
|
|
539
|
+
.build()
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Customer cohort retention query
|
|
543
|
+
*/
|
|
544
|
+
export const CohortRetention = query('cohort_retention', 'customer_events')
|
|
545
|
+
.describe('Customer retention by signup cohort')
|
|
546
|
+
.dimensions('cohort', 'month')
|
|
547
|
+
.measures('customers', 'mrr')
|
|
548
|
+
.last('12m')
|
|
549
|
+
.sort('cohort', 'asc')
|
|
550
|
+
.build()
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Unit economics query
|
|
554
|
+
*/
|
|
555
|
+
export const UnitEconomics = query('unit_economics', 'financial_events')
|
|
556
|
+
.describe('Key unit economics metrics')
|
|
557
|
+
.dimensions('month')
|
|
558
|
+
.measures('cac', 'ltv', 'ltvCacRatio', 'arpu', 'customerChurnRate')
|
|
559
|
+
.last('12m')
|
|
560
|
+
.sort('month', 'asc')
|
|
561
|
+
.build()
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Revenue by channel query
|
|
565
|
+
*/
|
|
566
|
+
export const RevenueByChannel = query('revenue_by_channel', 'revenue_events')
|
|
567
|
+
.describe('Revenue breakdown by acquisition channel')
|
|
568
|
+
.dimensions('channel')
|
|
569
|
+
.measures('mrr', 'newCustomers', 'cac')
|
|
570
|
+
.last('3m')
|
|
571
|
+
.sort('mrr', 'desc')
|
|
572
|
+
.build()
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Growth metrics query
|
|
576
|
+
*/
|
|
577
|
+
export const GrowthMetrics = query('growth_metrics', 'financial_events')
|
|
578
|
+
.describe('Key growth and efficiency metrics')
|
|
579
|
+
.dimensions('month')
|
|
580
|
+
.measures('mrr', 'netNewMrr', 'quickRatio', 'nrr', 'magicNumber')
|
|
581
|
+
.last('12m')
|
|
582
|
+
.sort('month', 'asc')
|
|
583
|
+
.build()
|
|
584
|
+
|
|
585
|
+
// =============================================================================
|
|
586
|
+
// View Builder
|
|
587
|
+
// =============================================================================
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Create a view from a query
|
|
591
|
+
*/
|
|
592
|
+
export function view(name: string, queryDef: Query): ViewBuilder {
|
|
593
|
+
return new ViewBuilder(name, queryDef)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Fluent view builder
|
|
598
|
+
*/
|
|
599
|
+
export class ViewBuilder {
|
|
600
|
+
private _view: View
|
|
601
|
+
|
|
602
|
+
constructor(name: string, queryDef: Query) {
|
|
603
|
+
this._view = { name, query: queryDef }
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
describe(description: string): this {
|
|
607
|
+
this._view.description = description
|
|
608
|
+
return this
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
materialize(refreshInterval?: string, retention?: string): this {
|
|
612
|
+
this._view.materialized = true
|
|
613
|
+
this._view.refreshInterval = refreshInterval
|
|
614
|
+
this._view.retention = retention
|
|
615
|
+
return this
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
public(): this {
|
|
619
|
+
this._view.public = true
|
|
620
|
+
return this
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
owner(owner: string): this {
|
|
624
|
+
this._view.owner = owner
|
|
625
|
+
return this
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
tags(...tags: string[]): this {
|
|
629
|
+
this._view.tags = tags
|
|
630
|
+
return this
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
build(): View {
|
|
634
|
+
return { ...this._view }
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// =============================================================================
|
|
639
|
+
// Dashboard Builder
|
|
640
|
+
// =============================================================================
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Create a dashboard
|
|
644
|
+
*/
|
|
645
|
+
export function dashboard(name: string): DashboardBuilder {
|
|
646
|
+
return new DashboardBuilder(name)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Fluent dashboard builder
|
|
651
|
+
*/
|
|
652
|
+
export class DashboardBuilder {
|
|
653
|
+
private _dashboard: Dashboard
|
|
654
|
+
|
|
655
|
+
constructor(name: string) {
|
|
656
|
+
this._dashboard = { name, views: [] }
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
describe(description: string): this {
|
|
660
|
+
this._dashboard.description = description
|
|
661
|
+
return this
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
add(viewDef: View, options?: { x?: number; y?: number; width?: number; height?: number; visualization?: Visualization }): this {
|
|
665
|
+
this._dashboard.views.push(viewDef)
|
|
666
|
+
if (options && this._dashboard.layout) {
|
|
667
|
+
this._dashboard.layout.items.push({
|
|
668
|
+
viewName: viewDef.name,
|
|
669
|
+
x: options.x || 0,
|
|
670
|
+
y: options.y || 0,
|
|
671
|
+
width: options.width || 1,
|
|
672
|
+
height: options.height || 1,
|
|
673
|
+
visualization: options.visualization,
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
return this
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
layout(columns: number, rows: number): this {
|
|
680
|
+
this._dashboard.layout = { columns, rows, items: [] }
|
|
681
|
+
return this
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
refresh(interval: string): this {
|
|
685
|
+
this._dashboard.refreshInterval = interval
|
|
686
|
+
return this
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
owner(owner: string): this {
|
|
690
|
+
this._dashboard.owner = owner
|
|
691
|
+
return this
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
tags(...tags: string[]): this {
|
|
695
|
+
this._dashboard.tags = tags
|
|
696
|
+
return this
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
build(): Dashboard {
|
|
700
|
+
return { ...this._dashboard }
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// =============================================================================
|
|
705
|
+
// Pre-built Dashboards
|
|
706
|
+
// =============================================================================
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Executive SaaS Dashboard
|
|
710
|
+
*/
|
|
711
|
+
export const ExecutiveDashboard = dashboard('executive')
|
|
712
|
+
.describe('Executive overview of key SaaS metrics')
|
|
713
|
+
.layout(4, 3)
|
|
714
|
+
.add(view('mrr', MrrOverview).build(), { x: 0, y: 0, width: 2, height: 1, visualization: 'trend' })
|
|
715
|
+
.add(view('arr_segments', ArrBySegment).build(), { x: 2, y: 0, width: 2, height: 1, visualization: 'bar' })
|
|
716
|
+
.add(view('unit_econ', UnitEconomics).build(), { x: 0, y: 1, width: 2, height: 1, visualization: 'table' })
|
|
717
|
+
.add(view('growth', GrowthMetrics).build(), { x: 2, y: 1, width: 2, height: 1, visualization: 'line' })
|
|
718
|
+
.add(view('cohorts', CohortRetention).build(), { x: 0, y: 2, width: 4, height: 1, visualization: 'cohort' })
|
|
719
|
+
.refresh('5m')
|
|
720
|
+
.tags('executive', 'saas', 'metrics')
|
|
721
|
+
.build()
|
|
722
|
+
|
|
723
|
+
// =============================================================================
|
|
724
|
+
// Query Execution Types (Interface with ai-database)
|
|
725
|
+
// =============================================================================
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Query result row
|
|
729
|
+
*/
|
|
730
|
+
export type QueryRow = Record<string, unknown>
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Query result
|
|
734
|
+
*/
|
|
735
|
+
export interface QueryResult {
|
|
736
|
+
query: Query
|
|
737
|
+
rows: QueryRow[]
|
|
738
|
+
rowCount: number
|
|
739
|
+
executionTimeMs: number
|
|
740
|
+
cached?: boolean
|
|
741
|
+
metadata?: Record<string, unknown>
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Query executor interface (implemented by ai-database)
|
|
746
|
+
*/
|
|
747
|
+
export interface QueryExecutor {
|
|
748
|
+
execute(query: Query): Promise<QueryResult>
|
|
749
|
+
explain(query: Query): Promise<string>
|
|
750
|
+
validate(query: Query): Promise<{ valid: boolean; errors?: string[] }>
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Streaming query result
|
|
755
|
+
*/
|
|
756
|
+
export interface StreamingQueryResult {
|
|
757
|
+
query: Query
|
|
758
|
+
stream: AsyncIterable<QueryRow>
|
|
759
|
+
cancel: () => void
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Streaming query executor interface
|
|
764
|
+
*/
|
|
765
|
+
export interface StreamingQueryExecutor extends QueryExecutor {
|
|
766
|
+
stream(query: Query): StreamingQueryResult
|
|
767
|
+
}
|