@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.
Files changed (237) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +361 -0
  4. package/bin/cli.ts +169 -0
  5. package/dist/Analytics.d.ts +558 -0
  6. package/dist/api.d.ts +109 -0
  7. package/dist/batching.d.ts +93 -0
  8. package/dist/chunk-2mx7fq49.js +4 -0
  9. package/dist/chunk-3z29508k.js +204 -0
  10. package/dist/chunk-deephkz6.js +56 -0
  11. package/dist/chunk-j261vgyp.js +305 -0
  12. package/dist/chunk-xga17tz7.js +207 -0
  13. package/dist/config.d.ts +132 -0
  14. package/dist/dashboard/components/index.d.ts +80 -0
  15. package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
  16. package/dist/dashboard/index.d.ts +110 -0
  17. package/dist/dashboard/types/index.d.ts +144 -0
  18. package/dist/dashboard/utils/index.d.ts +81 -0
  19. package/dist/dynamodb.d.ts +86 -0
  20. package/dist/funnels.d.ts +104 -0
  21. package/dist/geolocation.d.ts +96 -0
  22. package/dist/index.d.ts +371 -0
  23. package/dist/index.js +10211 -0
  24. package/dist/infrastructure/cdk.d.ts +60 -0
  25. package/dist/infrastructure/cloudformation.d.ts +44 -0
  26. package/dist/infrastructure/index.d.ts +45 -0
  27. package/dist/infrastructure/setup.d.ts +133 -0
  28. package/dist/integrations/cloudflare.d.ts +76 -0
  29. package/dist/integrations/hono.d.ts +60 -0
  30. package/dist/integrations/index.d.ts +24 -0
  31. package/dist/integrations/nuxt.d.ts +7 -0
  32. package/dist/integrations/nuxt.js +42 -0
  33. package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
  34. package/dist/integrations/runtime/use-ts-analytics.js +17 -0
  35. package/dist/integrations/stx.d.ts +88 -0
  36. package/dist/integrations/stx.js +18 -0
  37. package/dist/lib/crypto-random.d.ts +4 -0
  38. package/dist/lib/salt.d.ts +16 -0
  39. package/dist/local.d.ts +56 -0
  40. package/dist/model-connector.d.ts +145 -0
  41. package/dist/models/AggregatedStats.d.ts +9 -0
  42. package/dist/models/CampaignStats.d.ts +9 -0
  43. package/dist/models/Conversion.d.ts +11 -0
  44. package/dist/models/CustomEvent.d.ts +11 -0
  45. package/dist/models/DeviceStats.d.ts +9 -0
  46. package/dist/models/EventStats.d.ts +9 -0
  47. package/dist/models/GeoStats.d.ts +9 -0
  48. package/dist/models/Goal.d.ts +9 -0
  49. package/dist/models/GoalStats.d.ts +9 -0
  50. package/dist/models/PageStats.d.ts +11 -0
  51. package/dist/models/PageView.d.ts +13 -0
  52. package/dist/models/RealtimeStats.d.ts +9 -0
  53. package/dist/models/ReferrerStats.d.ts +9 -0
  54. package/dist/models/Session.d.ts +11 -0
  55. package/dist/models/Site.d.ts +11 -0
  56. package/dist/models/index.d.ts +28 -0
  57. package/dist/models/types.d.ts +60 -0
  58. package/dist/sqs-buffering.d.ts +243 -0
  59. package/dist/stacks-integration.d.ts +159 -0
  60. package/dist/tracking-script.d.ts +71 -0
  61. package/dist/tracking.d.ts +17 -0
  62. package/dist/tracking.js +26 -0
  63. package/dist/types.d.ts +595 -0
  64. package/dist/utils/geolocation.d.ts +111 -0
  65. package/dist/utils/user-agent.d.ts +17 -0
  66. package/dist/version.d.ts +7 -0
  67. package/package.json +119 -0
  68. package/src/Analytics.ts +3349 -0
  69. package/src/api.ts +1286 -0
  70. package/src/assets/crosswind.css +2220 -0
  71. package/src/batching.ts +452 -0
  72. package/src/components/dashboard/index.ts +18 -0
  73. package/src/config.ts +456 -0
  74. package/src/dashboard/Dashboard.stx +1517 -0
  75. package/src/dashboard/components/AlertCard.stx +177 -0
  76. package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
  77. package/src/dashboard/components/AnimatedNumber.stx +86 -0
  78. package/src/dashboard/components/BarChart.stx +220 -0
  79. package/src/dashboard/components/BrowserBreakdown.stx +98 -0
  80. package/src/dashboard/components/BrowsersTable.stx +125 -0
  81. package/src/dashboard/components/CampaignBreakdown.stx +163 -0
  82. package/src/dashboard/components/CampaignTable.stx +238 -0
  83. package/src/dashboard/components/CountryList.stx +101 -0
  84. package/src/dashboard/components/DataTable.stx +226 -0
  85. package/src/dashboard/components/DateRangePicker.stx +77 -0
  86. package/src/dashboard/components/DeviceBreakdown.stx +94 -0
  87. package/src/dashboard/components/DevicesTable.stx +163 -0
  88. package/src/dashboard/components/DonutChart.stories.ts +55 -0
  89. package/src/dashboard/components/DonutChart.stx +157 -0
  90. package/src/dashboard/components/EmptyState.stx +90 -0
  91. package/src/dashboard/components/EngagementMetrics.stx +183 -0
  92. package/src/dashboard/components/EventsSection.stx +192 -0
  93. package/src/dashboard/components/FilterBar.stx +104 -0
  94. package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
  95. package/src/dashboard/components/FunnelChart.stx +142 -0
  96. package/src/dashboard/components/GeoTable.stx +337 -0
  97. package/src/dashboard/components/GoalsPanel.stx +109 -0
  98. package/src/dashboard/components/Header.stx +39 -0
  99. package/src/dashboard/components/HeatmapChart.stx +140 -0
  100. package/src/dashboard/components/LiveActivityFeed.stx +172 -0
  101. package/src/dashboard/components/MetricComparison.stx +106 -0
  102. package/src/dashboard/components/MiniStats.stx +96 -0
  103. package/src/dashboard/components/OSBreakdown.stx +124 -0
  104. package/src/dashboard/components/PageDetailCard.stx +102 -0
  105. package/src/dashboard/components/PagesTable.stx +127 -0
  106. package/src/dashboard/components/ProgressRing.stx +89 -0
  107. package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
  108. package/src/dashboard/components/RealtimeCounter.stx +46 -0
  109. package/src/dashboard/components/ReferrersTable.stx +180 -0
  110. package/src/dashboard/components/SparklineChart.stx +160 -0
  111. package/src/dashboard/components/StatCard.stories.ts +58 -0
  112. package/src/dashboard/components/StatCard.stx +90 -0
  113. package/src/dashboard/components/SummaryStats.stx +81 -0
  114. package/src/dashboard/components/TabNav.stx +66 -0
  115. package/src/dashboard/components/ThemeSwitcher.stx +124 -0
  116. package/src/dashboard/components/TimeSeriesChart.stx +106 -0
  117. package/src/dashboard/components/TopList.stories.ts +58 -0
  118. package/src/dashboard/components/TopList.stx +74 -0
  119. package/src/dashboard/components/TrendIndicator.stx +84 -0
  120. package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
  121. package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
  122. package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
  123. package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
  124. package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
  125. package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
  126. package/src/dashboard/components/heatmap/index.ts +12 -0
  127. package/src/dashboard/components/index.ts +86 -0
  128. package/src/dashboard/composables/useAnalytics.ts +465 -0
  129. package/src/dashboard/demo/DemoApp.stx +370 -0
  130. package/src/dashboard/demo/index.ts +8 -0
  131. package/src/dashboard/demo/mockData.ts +234 -0
  132. package/src/dashboard/index.ts +117 -0
  133. package/src/dashboard/stx-shim.d.ts +4 -0
  134. package/src/dashboard/types/index.ts +203 -0
  135. package/src/dashboard/utils/index.ts +426 -0
  136. package/src/dynamodb.ts +344 -0
  137. package/src/funnels.ts +534 -0
  138. package/src/geolocation.ts +515 -0
  139. package/src/handlers/alerts.ts +328 -0
  140. package/src/handlers/annotations.ts +128 -0
  141. package/src/handlers/api-keys.ts +240 -0
  142. package/src/handlers/auth.ts +1020 -0
  143. package/src/handlers/authz.ts +137 -0
  144. package/src/handlers/collect.ts +724 -0
  145. package/src/handlers/data.ts +625 -0
  146. package/src/handlers/experiments.ts +138 -0
  147. package/src/handlers/funnels.ts +216 -0
  148. package/src/handlers/goals.ts +218 -0
  149. package/src/handlers/heatmaps.ts +272 -0
  150. package/src/handlers/index.ts +56 -0
  151. package/src/handlers/lib/read-cache.ts +63 -0
  152. package/src/handlers/misc.ts +486 -0
  153. package/src/handlers/oauth.ts +233 -0
  154. package/src/handlers/performance.ts +388 -0
  155. package/src/handlers/sessions.ts +413 -0
  156. package/src/handlers/sharing.ts +198 -0
  157. package/src/handlers/stats.ts +1368 -0
  158. package/src/handlers/team.ts +161 -0
  159. package/src/handlers/uptime.ts +283 -0
  160. package/src/handlers/views.ts +390 -0
  161. package/src/handlers/webhooks.ts +226 -0
  162. package/src/heatmap/index.ts +32 -0
  163. package/src/heatmap/tracking-script.ts +452 -0
  164. package/src/heatmap/types.ts +79 -0
  165. package/src/index.ts +387 -0
  166. package/src/infrastructure/cdk.ts +496 -0
  167. package/src/infrastructure/cloudformation.ts +595 -0
  168. package/src/infrastructure/index.ts +48 -0
  169. package/src/infrastructure/setup.ts +611 -0
  170. package/src/integrations/cloudflare.ts +732 -0
  171. package/src/integrations/hono.ts +589 -0
  172. package/src/integrations/index.ts +27 -0
  173. package/src/integrations/nuxt.ts +78 -0
  174. package/src/integrations/runtime/use-ts-analytics.ts +32 -0
  175. package/src/integrations/stx.ts +138 -0
  176. package/src/jobs/index.ts +127 -0
  177. package/src/lib/crypto-random.ts +41 -0
  178. package/src/lib/ddb-errors.ts +20 -0
  179. package/src/lib/dynamodb.ts +216 -0
  180. package/src/lib/email.ts +38 -0
  181. package/src/lib/ga-import.ts +471 -0
  182. package/src/lib/ga4-api.ts +244 -0
  183. package/src/lib/goals.ts +205 -0
  184. package/src/lib/index.ts +6 -0
  185. package/src/lib/ingest-counters.ts +123 -0
  186. package/src/lib/log.ts +36 -0
  187. package/src/lib/membership.ts +98 -0
  188. package/src/lib/plans.ts +90 -0
  189. package/src/lib/quota.ts +46 -0
  190. package/src/lib/rate-limit.ts +27 -0
  191. package/src/lib/rollups.ts +472 -0
  192. package/src/lib/salt.ts +97 -0
  193. package/src/lib/scheduler.ts +97 -0
  194. package/src/lib/significance.ts +66 -0
  195. package/src/lib/site-retention.ts +56 -0
  196. package/src/local.ts +360 -0
  197. package/src/model-connector.ts +616 -0
  198. package/src/models/AggregatedStats.ts +162 -0
  199. package/src/models/CampaignStats.ts +141 -0
  200. package/src/models/Conversion.ts +135 -0
  201. package/src/models/CustomEvent.ts +123 -0
  202. package/src/models/DeviceStats.ts +105 -0
  203. package/src/models/EventStats.ts +108 -0
  204. package/src/models/GeoStats.ts +112 -0
  205. package/src/models/Goal.ts +105 -0
  206. package/src/models/GoalStats.ts +106 -0
  207. package/src/models/PageStats.ts +152 -0
  208. package/src/models/PageView.ts +277 -0
  209. package/src/models/RealtimeStats.ts +96 -0
  210. package/src/models/ReferrerStats.ts +115 -0
  211. package/src/models/Session.ts +235 -0
  212. package/src/models/Site.ts +115 -0
  213. package/src/models/index.ts +50 -0
  214. package/src/models/orm/index.ts +2043 -0
  215. package/src/models/types.ts +71 -0
  216. package/src/router.ts +521 -0
  217. package/src/sqs-buffering.ts +806 -0
  218. package/src/stacks-integration.ts +527 -0
  219. package/src/tracking-script.ts +944 -0
  220. package/src/tracking.ts +27 -0
  221. package/src/types/analytics.ts +219 -0
  222. package/src/types/api.ts +75 -0
  223. package/src/types/bun-router.d.ts +15 -0
  224. package/src/types/dashboard.ts +62 -0
  225. package/src/types/index.ts +66 -0
  226. package/src/types/stx.d.ts +27 -0
  227. package/src/types/window.d.ts +101 -0
  228. package/src/types.ts +911 -0
  229. package/src/utils/cache.ts +148 -0
  230. package/src/utils/date.ts +118 -0
  231. package/src/utils/filters.ts +71 -0
  232. package/src/utils/geolocation.ts +323 -0
  233. package/src/utils/index.ts +9 -0
  234. package/src/utils/response.ts +180 -0
  235. package/src/utils/timezone-country.ts +242 -0
  236. package/src/utils/user-agent.ts +110 -0
  237. 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
+ }