@stacksjs/ts-analytics 0.1.6
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/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Model Connector
|
|
3
|
+
*
|
|
4
|
+
* Bridges analytics Stacks models with the dynamodb-tooling model parser
|
|
5
|
+
* and single-table design generator. This allows the existing tooling
|
|
6
|
+
* to work with analytics models.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
AggregatedStatsModel,
|
|
11
|
+
analyticsModels,
|
|
12
|
+
CampaignStatsModel,
|
|
13
|
+
ConversionModel,
|
|
14
|
+
CustomEventModel,
|
|
15
|
+
DeviceStatsModel,
|
|
16
|
+
EventStatsModel,
|
|
17
|
+
GeoStatsModel,
|
|
18
|
+
GoalModel,
|
|
19
|
+
GoalStatsModel,
|
|
20
|
+
PageStatsModel,
|
|
21
|
+
PageViewModel,
|
|
22
|
+
RealtimeStatsModel,
|
|
23
|
+
ReferrerStatsModel,
|
|
24
|
+
SessionModel,
|
|
25
|
+
SiteModel,
|
|
26
|
+
} from './models'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Types
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stacks model definition (matches model-parser types)
|
|
34
|
+
*/
|
|
35
|
+
export interface StacksModelDefinition {
|
|
36
|
+
name: string
|
|
37
|
+
table?: string
|
|
38
|
+
primaryKey?: string | string[]
|
|
39
|
+
autoIncrement?: boolean
|
|
40
|
+
attributes: Record<string, AttributeDefinition>
|
|
41
|
+
hasOne?: string[]
|
|
42
|
+
hasMany?: string[]
|
|
43
|
+
belongsTo?: string[]
|
|
44
|
+
belongsToMany?: string[]
|
|
45
|
+
traits?: ModelTraits
|
|
46
|
+
dynamodb?: DynamoDBKeyPatterns
|
|
47
|
+
indexes?: IndexDefinition[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AttributeDefinition {
|
|
51
|
+
required?: boolean
|
|
52
|
+
unique?: boolean
|
|
53
|
+
fillable?: boolean
|
|
54
|
+
hidden?: boolean
|
|
55
|
+
default?: unknown
|
|
56
|
+
validation?: {
|
|
57
|
+
rule?: string
|
|
58
|
+
message?: Record<string, string>
|
|
59
|
+
}
|
|
60
|
+
cast?: string
|
|
61
|
+
comment?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ModelTraits {
|
|
65
|
+
useUuid?: boolean
|
|
66
|
+
useTimestamps?: boolean
|
|
67
|
+
useSoftDeletes?: boolean
|
|
68
|
+
useTtl?: boolean
|
|
69
|
+
useVersioning?: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DynamoDBKeyPatterns {
|
|
73
|
+
pk: (entity: Record<string, unknown>) => string
|
|
74
|
+
sk: (entity: Record<string, unknown>) => string
|
|
75
|
+
gsi1pk?: (entity: Record<string, unknown>) => string
|
|
76
|
+
gsi1sk?: (entity: Record<string, unknown>) => string
|
|
77
|
+
gsi2pk?: (entity: Record<string, unknown>) => string
|
|
78
|
+
gsi2sk?: (entity: Record<string, unknown>) => string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface IndexDefinition {
|
|
82
|
+
name: string
|
|
83
|
+
columns: string[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Access pattern definition
|
|
88
|
+
*/
|
|
89
|
+
export interface AccessPattern {
|
|
90
|
+
name: string
|
|
91
|
+
description: string
|
|
92
|
+
operation: 'Get' | 'Query' | 'Scan'
|
|
93
|
+
index?: 'main' | 'GSI1' | 'GSI2'
|
|
94
|
+
keyCondition: string
|
|
95
|
+
filterExpression?: string
|
|
96
|
+
example?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Single-table design output
|
|
101
|
+
*/
|
|
102
|
+
export interface SingleTableDesign {
|
|
103
|
+
tableName: string
|
|
104
|
+
keySchema: {
|
|
105
|
+
partitionKey: { name: string, type: 'S' | 'N' | 'B' }
|
|
106
|
+
sortKey: { name: string, type: 'S' | 'N' | 'B' }
|
|
107
|
+
}
|
|
108
|
+
globalSecondaryIndexes: Array<{
|
|
109
|
+
indexName: string
|
|
110
|
+
partitionKey: { name: string, type: 'S' | 'N' | 'B' }
|
|
111
|
+
sortKey: { name: string, type: 'S' | 'N' | 'B' }
|
|
112
|
+
projection: 'ALL' | 'KEYS_ONLY' | string[]
|
|
113
|
+
}>
|
|
114
|
+
entities: EntityDesign[]
|
|
115
|
+
accessPatterns: AccessPattern[]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface EntityDesign {
|
|
119
|
+
name: string
|
|
120
|
+
keyPattern: {
|
|
121
|
+
pk: string
|
|
122
|
+
sk: string
|
|
123
|
+
gsi1pk?: string
|
|
124
|
+
gsi1sk?: string
|
|
125
|
+
gsi2pk?: string
|
|
126
|
+
gsi2sk?: string
|
|
127
|
+
}
|
|
128
|
+
attributes: Array<{
|
|
129
|
+
name: string
|
|
130
|
+
type: string
|
|
131
|
+
dynamoType: 'S' | 'N' | 'B' | 'BOOL' | 'L' | 'M' | 'SS' | 'NS' | 'BS'
|
|
132
|
+
required: boolean
|
|
133
|
+
}>
|
|
134
|
+
relationships: Array<{
|
|
135
|
+
type: 'hasOne' | 'hasMany' | 'belongsTo' | 'belongsToMany'
|
|
136
|
+
target: string
|
|
137
|
+
foreignKey?: string
|
|
138
|
+
}>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Model Registry
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Map of all analytics models
|
|
147
|
+
*/
|
|
148
|
+
export const analyticsModelRegistry: Record<string, StacksModelDefinition> = {
|
|
149
|
+
Site: SiteModel as unknown as StacksModelDefinition,
|
|
150
|
+
PageView: PageViewModel as unknown as StacksModelDefinition,
|
|
151
|
+
Session: SessionModel as unknown as StacksModelDefinition,
|
|
152
|
+
CustomEvent: CustomEventModel as unknown as StacksModelDefinition,
|
|
153
|
+
Goal: GoalModel as unknown as StacksModelDefinition,
|
|
154
|
+
Conversion: ConversionModel as unknown as StacksModelDefinition,
|
|
155
|
+
AggregatedStats: AggregatedStatsModel as unknown as StacksModelDefinition,
|
|
156
|
+
PageStats: PageStatsModel as unknown as StacksModelDefinition,
|
|
157
|
+
ReferrerStats: ReferrerStatsModel as unknown as StacksModelDefinition,
|
|
158
|
+
GeoStats: GeoStatsModel as unknown as StacksModelDefinition,
|
|
159
|
+
DeviceStats: DeviceStatsModel as unknown as StacksModelDefinition,
|
|
160
|
+
CampaignStats: CampaignStatsModel as unknown as StacksModelDefinition,
|
|
161
|
+
EventStats: EventStatsModel as unknown as StacksModelDefinition,
|
|
162
|
+
GoalStats: GoalStatsModel as unknown as StacksModelDefinition,
|
|
163
|
+
RealtimeStats: RealtimeStatsModel as unknown as StacksModelDefinition,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get all analytics model names
|
|
168
|
+
*/
|
|
169
|
+
export function getAnalyticsModelNames(): string[] {
|
|
170
|
+
return [...analyticsModels]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get a specific analytics model
|
|
175
|
+
*/
|
|
176
|
+
export function getAnalyticsModel(name: string): StacksModelDefinition | undefined {
|
|
177
|
+
return analyticsModelRegistry[name]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get all analytics models
|
|
182
|
+
*/
|
|
183
|
+
export function getAllAnalyticsModels(): StacksModelDefinition[] {
|
|
184
|
+
return Object.values(analyticsModelRegistry)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Single-Table Design Generator
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Generate single-table design from analytics models
|
|
193
|
+
*/
|
|
194
|
+
export function generateAnalyticsSingleTableDesign(
|
|
195
|
+
tableName = 'analytics',
|
|
196
|
+
): SingleTableDesign {
|
|
197
|
+
const entities: EntityDesign[] = []
|
|
198
|
+
const accessPatterns: AccessPattern[] = []
|
|
199
|
+
|
|
200
|
+
for (const [name, model] of Object.entries(analyticsModelRegistry)) {
|
|
201
|
+
// Generate entity design
|
|
202
|
+
const entity = generateEntityDesign(name, model)
|
|
203
|
+
entities.push(entity)
|
|
204
|
+
|
|
205
|
+
// Generate access patterns for this entity
|
|
206
|
+
const patterns = generateAccessPatterns(name, model)
|
|
207
|
+
accessPatterns.push(...patterns)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
tableName,
|
|
212
|
+
keySchema: {
|
|
213
|
+
partitionKey: { name: 'pk', type: 'S' },
|
|
214
|
+
sortKey: { name: 'sk', type: 'S' },
|
|
215
|
+
},
|
|
216
|
+
globalSecondaryIndexes: [
|
|
217
|
+
{
|
|
218
|
+
indexName: 'GSI1',
|
|
219
|
+
partitionKey: { name: 'gsi1pk', type: 'S' },
|
|
220
|
+
sortKey: { name: 'gsi1sk', type: 'S' },
|
|
221
|
+
projection: 'ALL',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
indexName: 'GSI2',
|
|
225
|
+
partitionKey: { name: 'gsi2pk', type: 'S' },
|
|
226
|
+
sortKey: { name: 'gsi2sk', type: 'S' },
|
|
227
|
+
projection: 'ALL',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
entities,
|
|
231
|
+
accessPatterns,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Generate entity design from model
|
|
237
|
+
*/
|
|
238
|
+
function generateEntityDesign(name: string, model: StacksModelDefinition): EntityDesign {
|
|
239
|
+
const attributes = Object.entries(model.attributes || {}).map(([attrName, attr]) => ({
|
|
240
|
+
name: attrName,
|
|
241
|
+
type: attr.cast || inferType(attr),
|
|
242
|
+
dynamoType: inferDynamoType(attr),
|
|
243
|
+
required: attr.required || false,
|
|
244
|
+
}))
|
|
245
|
+
|
|
246
|
+
const relationships: EntityDesign['relationships'] = []
|
|
247
|
+
|
|
248
|
+
if (model.hasOne) {
|
|
249
|
+
relationships.push(...model.hasOne.map(target => ({
|
|
250
|
+
type: 'hasOne' as const,
|
|
251
|
+
target,
|
|
252
|
+
})))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (model.hasMany) {
|
|
256
|
+
relationships.push(...model.hasMany.map(target => ({
|
|
257
|
+
type: 'hasMany' as const,
|
|
258
|
+
target,
|
|
259
|
+
})))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (model.belongsTo) {
|
|
263
|
+
relationships.push(...model.belongsTo.map(target => ({
|
|
264
|
+
type: 'belongsTo' as const,
|
|
265
|
+
target,
|
|
266
|
+
foreignKey: `${target.toLowerCase()}Id`,
|
|
267
|
+
})))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Extract key patterns from dynamodb config
|
|
271
|
+
const keyPattern = extractKeyPatterns(name, model)
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
name,
|
|
275
|
+
keyPattern,
|
|
276
|
+
attributes,
|
|
277
|
+
relationships,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Extract key patterns from model's dynamodb config
|
|
283
|
+
*/
|
|
284
|
+
function extractKeyPatterns(
|
|
285
|
+
name: string,
|
|
286
|
+
model: StacksModelDefinition,
|
|
287
|
+
): EntityDesign['keyPattern'] {
|
|
288
|
+
// Default patterns based on entity name
|
|
289
|
+
const entityPrefix = name.toUpperCase()
|
|
290
|
+
const defaultPatterns = {
|
|
291
|
+
pk: `${entityPrefix}#{id}`,
|
|
292
|
+
sk: `${entityPrefix}#{id}`,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// If model has dynamodb key functions, extract pattern descriptions
|
|
296
|
+
if (model.dynamodb) {
|
|
297
|
+
// These are functions, so we provide descriptive patterns
|
|
298
|
+
// based on the analytics key pattern conventions
|
|
299
|
+
return getAnalyticsKeyPatterns(name)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return defaultPatterns
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get predefined key patterns for analytics entities
|
|
307
|
+
*/
|
|
308
|
+
function getAnalyticsKeyPatterns(entityName: string): EntityDesign['keyPattern'] {
|
|
309
|
+
const patterns: Record<string, EntityDesign['keyPattern']> = {
|
|
310
|
+
Site: {
|
|
311
|
+
pk: 'SITE#{id}',
|
|
312
|
+
sk: 'SITE#{id}',
|
|
313
|
+
gsi1pk: 'OWNER#{ownerId}',
|
|
314
|
+
gsi1sk: 'SITE#{id}',
|
|
315
|
+
},
|
|
316
|
+
PageView: {
|
|
317
|
+
pk: 'SITE#{siteId}',
|
|
318
|
+
sk: 'PV#{timestamp}#{id}',
|
|
319
|
+
gsi1pk: 'SITE#{siteId}#DATE#{date}',
|
|
320
|
+
gsi1sk: 'PATH#{path}#{id}',
|
|
321
|
+
gsi2pk: 'SITE#{siteId}#VISITOR#{visitorId}',
|
|
322
|
+
gsi2sk: 'PV#{timestamp}',
|
|
323
|
+
},
|
|
324
|
+
Session: {
|
|
325
|
+
pk: 'SITE#{siteId}',
|
|
326
|
+
sk: 'SESSION#{id}',
|
|
327
|
+
gsi1pk: 'SITE#{siteId}#SESSIONS#{date}',
|
|
328
|
+
gsi1sk: 'SESSION#{id}',
|
|
329
|
+
},
|
|
330
|
+
CustomEvent: {
|
|
331
|
+
pk: 'SITE#{siteId}',
|
|
332
|
+
sk: 'EVENT#{timestamp}#{id}',
|
|
333
|
+
gsi1pk: 'SITE#{siteId}#EVENTNAME#{name}',
|
|
334
|
+
gsi1sk: 'EVENT#{timestamp}',
|
|
335
|
+
},
|
|
336
|
+
Goal: {
|
|
337
|
+
pk: 'SITE#{siteId}',
|
|
338
|
+
sk: 'GOAL#{id}',
|
|
339
|
+
},
|
|
340
|
+
Conversion: {
|
|
341
|
+
pk: 'SITE#{siteId}',
|
|
342
|
+
sk: 'CONVERSION#{timestamp}#{id}',
|
|
343
|
+
gsi1pk: 'SITE#{siteId}#GOAL#{goalId}',
|
|
344
|
+
gsi1sk: 'CONVERSION#{timestamp}',
|
|
345
|
+
},
|
|
346
|
+
AggregatedStats: {
|
|
347
|
+
pk: 'SITE#{siteId}',
|
|
348
|
+
sk: 'STATS#{period}#{periodStart}',
|
|
349
|
+
},
|
|
350
|
+
PageStats: {
|
|
351
|
+
pk: 'SITE#{siteId}',
|
|
352
|
+
sk: 'PAGESTATS#{period}#{periodStart}#{path}',
|
|
353
|
+
gsi1pk: 'SITE#{siteId}#PAGESTATS#{period}#{periodStart}',
|
|
354
|
+
gsi1sk: 'PV#{pageViews}#{path}',
|
|
355
|
+
},
|
|
356
|
+
ReferrerStats: {
|
|
357
|
+
pk: 'SITE#{siteId}',
|
|
358
|
+
sk: 'REFSTATS#{period}#{periodStart}#{source}',
|
|
359
|
+
},
|
|
360
|
+
GeoStats: {
|
|
361
|
+
pk: 'SITE#{siteId}',
|
|
362
|
+
sk: 'GEOSTATS#{period}#{periodStart}#{country}',
|
|
363
|
+
},
|
|
364
|
+
DeviceStats: {
|
|
365
|
+
pk: 'SITE#{siteId}',
|
|
366
|
+
sk: 'DEVICESTATS#{period}#{periodStart}#{dimension}#{value}',
|
|
367
|
+
},
|
|
368
|
+
CampaignStats: {
|
|
369
|
+
pk: 'SITE#{siteId}',
|
|
370
|
+
sk: 'CAMPSTATS#{period}#{periodStart}#{utmSource}',
|
|
371
|
+
},
|
|
372
|
+
EventStats: {
|
|
373
|
+
pk: 'SITE#{siteId}',
|
|
374
|
+
sk: 'EVENTSTATS#{period}#{periodStart}#{eventName}',
|
|
375
|
+
},
|
|
376
|
+
GoalStats: {
|
|
377
|
+
pk: 'SITE#{siteId}',
|
|
378
|
+
sk: 'GOALSTATS#{goalId}#{period}#{periodStart}',
|
|
379
|
+
},
|
|
380
|
+
RealtimeStats: {
|
|
381
|
+
pk: 'SITE#{siteId}',
|
|
382
|
+
sk: 'REALTIME#{minute}',
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return patterns[entityName] || { pk: `${entityName.toUpperCase()}#{id}`, sk: `${entityName.toUpperCase()}#{id}` }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Generate access patterns for an entity
|
|
391
|
+
*/
|
|
392
|
+
function generateAccessPatterns(name: string, model: StacksModelDefinition): AccessPattern[] {
|
|
393
|
+
const patterns: AccessPattern[] = []
|
|
394
|
+
const keyPattern = getAnalyticsKeyPatterns(name)
|
|
395
|
+
|
|
396
|
+
// Get by primary key
|
|
397
|
+
patterns.push({
|
|
398
|
+
name: `Get${name}ById`,
|
|
399
|
+
description: `Get a single ${name} by its primary key`,
|
|
400
|
+
operation: 'Get',
|
|
401
|
+
index: 'main',
|
|
402
|
+
keyCondition: `pk = ${keyPattern.pk} AND sk = ${keyPattern.sk}`,
|
|
403
|
+
example: `Get ${name} where id = "abc123"`,
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
// Query by partition key (list all with same pk)
|
|
407
|
+
patterns.push({
|
|
408
|
+
name: `List${name}sByPartition`,
|
|
409
|
+
description: `List all ${name}s for a partition`,
|
|
410
|
+
operation: 'Query',
|
|
411
|
+
index: 'main',
|
|
412
|
+
keyCondition: `pk = ${keyPattern.pk}`,
|
|
413
|
+
example: `List all ${name}s for site`,
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
// GSI1 patterns
|
|
417
|
+
if (keyPattern.gsi1pk) {
|
|
418
|
+
patterns.push({
|
|
419
|
+
name: `Query${name}ByGSI1`,
|
|
420
|
+
description: `Query ${name}s using GSI1`,
|
|
421
|
+
operation: 'Query',
|
|
422
|
+
index: 'GSI1',
|
|
423
|
+
keyCondition: `gsi1pk = ${keyPattern.gsi1pk}`,
|
|
424
|
+
example: `Query ${name}s via secondary index`,
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// GSI2 patterns
|
|
429
|
+
if (keyPattern.gsi2pk) {
|
|
430
|
+
patterns.push({
|
|
431
|
+
name: `Query${name}ByGSI2`,
|
|
432
|
+
description: `Query ${name}s using GSI2`,
|
|
433
|
+
operation: 'Query',
|
|
434
|
+
index: 'GSI2',
|
|
435
|
+
keyCondition: `gsi2pk = ${keyPattern.gsi2pk}`,
|
|
436
|
+
example: `Query ${name}s via tertiary index`,
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Relationship-based patterns
|
|
441
|
+
if (model.belongsTo) {
|
|
442
|
+
for (const parent of model.belongsTo) {
|
|
443
|
+
patterns.push({
|
|
444
|
+
name: `Get${name}sFor${parent}`,
|
|
445
|
+
description: `Get all ${name}s belonging to a ${parent}`,
|
|
446
|
+
operation: 'Query',
|
|
447
|
+
index: 'main',
|
|
448
|
+
keyCondition: `pk = ${parent.toUpperCase()}#{${parent.toLowerCase()}Id}`,
|
|
449
|
+
example: `Get all ${name}s for ${parent} with id "xyz"`,
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return patterns
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Infer TypeScript type from attribute definition
|
|
459
|
+
*/
|
|
460
|
+
function inferType(attr: AttributeDefinition): string {
|
|
461
|
+
if (attr.cast) {
|
|
462
|
+
const castMap: Record<string, string> = {
|
|
463
|
+
integer: 'number',
|
|
464
|
+
float: 'number',
|
|
465
|
+
number: 'number',
|
|
466
|
+
string: 'string',
|
|
467
|
+
boolean: 'boolean',
|
|
468
|
+
array: 'array',
|
|
469
|
+
json: 'object',
|
|
470
|
+
datetime: 'Date',
|
|
471
|
+
date: 'Date',
|
|
472
|
+
}
|
|
473
|
+
return castMap[attr.cast] || 'string'
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (attr.validation?.rule) {
|
|
477
|
+
const rule = attr.validation.rule
|
|
478
|
+
if (rule.includes('number') || rule.includes('integer'))
|
|
479
|
+
return 'number'
|
|
480
|
+
if (rule.includes('boolean'))
|
|
481
|
+
return 'boolean'
|
|
482
|
+
if (rule.includes('array'))
|
|
483
|
+
return 'array'
|
|
484
|
+
if (rule.includes('date'))
|
|
485
|
+
return 'Date'
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return 'string'
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Infer DynamoDB type from attribute definition
|
|
493
|
+
*/
|
|
494
|
+
function inferDynamoType(attr: AttributeDefinition): 'S' | 'N' | 'B' | 'BOOL' | 'L' | 'M' | 'SS' | 'NS' | 'BS' {
|
|
495
|
+
const type = inferType(attr)
|
|
496
|
+
|
|
497
|
+
const typeMap: Record<string, 'S' | 'N' | 'B' | 'BOOL' | 'L' | 'M'> = {
|
|
498
|
+
string: 'S',
|
|
499
|
+
number: 'N',
|
|
500
|
+
boolean: 'BOOL',
|
|
501
|
+
array: 'L',
|
|
502
|
+
object: 'M',
|
|
503
|
+
Date: 'S', // Store dates as ISO strings
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return typeMap[type] || 'S'
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ============================================================================
|
|
510
|
+
// Documentation Generator
|
|
511
|
+
// ============================================================================
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Generate markdown documentation for analytics single-table design
|
|
515
|
+
*/
|
|
516
|
+
export function generateAnalyticsDesignDoc(): string {
|
|
517
|
+
const design = generateAnalyticsSingleTableDesign()
|
|
518
|
+
|
|
519
|
+
let doc = `# Analytics Single-Table Design
|
|
520
|
+
|
|
521
|
+
## Table Schema
|
|
522
|
+
|
|
523
|
+
**Table Name:** \`${design.tableName}\`
|
|
524
|
+
|
|
525
|
+
### Key Schema
|
|
526
|
+
- **Partition Key (pk):** String
|
|
527
|
+
- **Sort Key (sk):** String
|
|
528
|
+
|
|
529
|
+
### Global Secondary Indexes
|
|
530
|
+
|
|
531
|
+
| Index | Partition Key | Sort Key | Projection |
|
|
532
|
+
|-------|--------------|----------|------------|
|
|
533
|
+
`
|
|
534
|
+
|
|
535
|
+
for (const gsi of design.globalSecondaryIndexes) {
|
|
536
|
+
doc += `| ${gsi.indexName} | ${gsi.partitionKey.name} | ${gsi.sortKey.name} | ${gsi.projection} |\n`
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
doc += `\n## Entities\n\n`
|
|
540
|
+
|
|
541
|
+
for (const entity of design.entities) {
|
|
542
|
+
doc += `### ${entity.name}\n\n`
|
|
543
|
+
doc += `**Key Pattern:**\n`
|
|
544
|
+
doc += `- pk: \`${entity.keyPattern.pk}\`\n`
|
|
545
|
+
doc += `- sk: \`${entity.keyPattern.sk}\`\n`
|
|
546
|
+
if (entity.keyPattern.gsi1pk) {
|
|
547
|
+
doc += `- gsi1pk: \`${entity.keyPattern.gsi1pk}\`\n`
|
|
548
|
+
doc += `- gsi1sk: \`${entity.keyPattern.gsi1sk}\`\n`
|
|
549
|
+
}
|
|
550
|
+
if (entity.keyPattern.gsi2pk) {
|
|
551
|
+
doc += `- gsi2pk: \`${entity.keyPattern.gsi2pk}\`\n`
|
|
552
|
+
doc += `- gsi2sk: \`${entity.keyPattern.gsi2sk}\`\n`
|
|
553
|
+
}
|
|
554
|
+
doc += `\n`
|
|
555
|
+
|
|
556
|
+
if (entity.attributes.length > 0) {
|
|
557
|
+
doc += `**Attributes:**\n\n`
|
|
558
|
+
doc += `| Name | Type | DynamoDB | Required |\n`
|
|
559
|
+
doc += `|------|------|----------|----------|\n`
|
|
560
|
+
for (const attr of entity.attributes) {
|
|
561
|
+
doc += `| ${attr.name} | ${attr.type} | ${attr.dynamoType} | ${attr.required ? 'Yes' : 'No'} |\n`
|
|
562
|
+
}
|
|
563
|
+
doc += `\n`
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (entity.relationships.length > 0) {
|
|
567
|
+
doc += `**Relationships:**\n`
|
|
568
|
+
for (const rel of entity.relationships) {
|
|
569
|
+
doc += `- ${rel.type}: ${rel.target}${rel.foreignKey ? ` (FK: ${rel.foreignKey})` : ''}\n`
|
|
570
|
+
}
|
|
571
|
+
doc += `\n`
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
doc += `## Access Patterns\n\n`
|
|
576
|
+
doc += `| Pattern | Operation | Index | Key Condition |\n`
|
|
577
|
+
doc += `|---------|-----------|-------|---------------|\n`
|
|
578
|
+
|
|
579
|
+
for (const pattern of design.accessPatterns) {
|
|
580
|
+
doc += `| ${pattern.name} | ${pattern.operation} | ${pattern.index || 'main'} | \`${pattern.keyCondition}\` |\n`
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return doc
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Generate access pattern matrix
|
|
588
|
+
*/
|
|
589
|
+
export function generateAccessPatternMatrix(): string {
|
|
590
|
+
const design = generateAnalyticsSingleTableDesign()
|
|
591
|
+
|
|
592
|
+
let matrix = `# Analytics Access Pattern Matrix
|
|
593
|
+
|
|
594
|
+
| Entity | Get By ID | List | GSI1 Query | GSI2 Query | Notes |
|
|
595
|
+
|--------|-----------|------|------------|------------|-------|
|
|
596
|
+
`
|
|
597
|
+
|
|
598
|
+
for (const entity of design.entities) {
|
|
599
|
+
const hasGsi1 = !!entity.keyPattern.gsi1pk
|
|
600
|
+
const hasGsi2 = !!entity.keyPattern.gsi2pk
|
|
601
|
+
|
|
602
|
+
matrix += `| ${entity.name} | ✅ | ✅ | ${hasGsi1 ? '✅' : '❌'} | ${hasGsi2 ? '✅' : '❌'} | `
|
|
603
|
+
|
|
604
|
+
const notes: string[] = []
|
|
605
|
+
if (entity.keyPattern.pk.includes('siteId')) {
|
|
606
|
+
notes.push('Site-scoped')
|
|
607
|
+
}
|
|
608
|
+
if (entity.keyPattern.sk.includes('timestamp')) {
|
|
609
|
+
notes.push('Time-ordered')
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
matrix += `${notes.join(', ')} |\n`
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return matrix
|
|
616
|
+
}
|