@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,611 @@
1
+ /**
2
+ * Analytics Table Setup & Migration Scripts
3
+ *
4
+ * Provides utilities for creating, updating, and migrating
5
+ * the analytics DynamoDB table.
6
+ */
7
+
8
+ import type { AnalyticsTableProps } from './cdk'
9
+ import { generateCreateTableInput } from './cdk'
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export interface SetupConfig extends AnalyticsTableProps {
16
+ /** AWS region */
17
+ region?: string
18
+ /** DynamoDB endpoint (for local development) */
19
+ endpoint?: string
20
+ /** Wait for table to be active */
21
+ waitForActive?: boolean
22
+ /** Timeout for waiting (ms) */
23
+ waitTimeout?: number
24
+ }
25
+
26
+ export interface SetupResult {
27
+ success: boolean
28
+ tableName: string
29
+ tableArn?: string
30
+ error?: string
31
+ }
32
+
33
+ export interface MigrationStep {
34
+ name: string
35
+ description: string
36
+ execute: (client: DynamoDBClientLike) => Promise<void>
37
+ rollback?: (client: DynamoDBClientLike) => Promise<void>
38
+ }
39
+
40
+ export interface MigrationResult {
41
+ success: boolean
42
+ stepsCompleted: string[]
43
+ error?: string
44
+ failedStep?: string
45
+ }
46
+
47
+ // ============================================================================
48
+ // Client Interface (for dependency injection)
49
+ // ============================================================================
50
+
51
+ /**
52
+ * Minimal DynamoDB client interface
53
+ * Allows using either AWS SDK v2 or v3
54
+ */
55
+ export interface DynamoDBClientLike {
56
+ send: (command: unknown) => Promise<unknown>
57
+ }
58
+
59
+ /**
60
+ * CreateTable command interface
61
+ */
62
+ export interface CreateTableCommandLike {
63
+ input: Record<string, unknown>
64
+ }
65
+
66
+ /**
67
+ * DescribeTable command interface
68
+ */
69
+ export interface DescribeTableCommandLike {
70
+ input: { TableName: string }
71
+ }
72
+
73
+ /**
74
+ * UpdateTimeToLive command interface
75
+ */
76
+ export interface UpdateTimeToLiveCommandLike {
77
+ input: {
78
+ TableName: string
79
+ TimeToLiveSpecification: {
80
+ AttributeName: string
81
+ Enabled: boolean
82
+ }
83
+ }
84
+ }
85
+
86
+ // ============================================================================
87
+ // Setup Functions
88
+ // ============================================================================
89
+
90
+ /**
91
+ * Create the analytics table
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * import { DynamoDBClient, CreateTableCommand } from '@aws-sdk/client-dynamodb'
96
+ * import { createAnalyticsTable } from 'dynamodb-tooling/analytics/infrastructure'
97
+ *
98
+ * const client = new DynamoDBClient({ region: 'us-east-1' })
99
+ *
100
+ * const result = await createAnalyticsTable(client, {
101
+ * tableName: 'my-analytics',
102
+ * billingMode: 'PAY_PER_REQUEST',
103
+ * })
104
+ *
105
+ * console.log(`Created table: ${result.tableName}`)
106
+ * ```
107
+ */
108
+ export async function createAnalyticsTable(
109
+ client: DynamoDBClientLike,
110
+ config: SetupConfig | undefined = {},
111
+ commandFactories: {
112
+ CreateTableCommand: new (input: Record<string, unknown>) => unknown
113
+ DescribeTableCommand: new (input: { TableName: string }) => unknown
114
+ },
115
+ ): Promise<SetupResult> {
116
+ const { CreateTableCommand, DescribeTableCommand } = commandFactories
117
+ const tableName = config.tableName || 'analytics'
118
+
119
+ try {
120
+ // Check if table exists
121
+ try {
122
+ await client.send(new DescribeTableCommand({ TableName: tableName }))
123
+ return {
124
+ success: true,
125
+ tableName,
126
+ error: 'Table already exists',
127
+ }
128
+ }
129
+ catch (err) {
130
+ // Table doesn't exist, continue with creation
131
+ if ((err as { name?: string }).name !== 'ResourceNotFoundException') {
132
+ throw err
133
+ }
134
+ }
135
+
136
+ // Create table
137
+ const createInput = generateCreateTableInput(config)
138
+ await client.send(new CreateTableCommand(createInput))
139
+
140
+ // Wait for table to be active
141
+ if (config.waitForActive !== false) {
142
+ const timeout = config.waitTimeout || 60000
143
+ const startTime = Date.now()
144
+
145
+ while (Date.now() - startTime < timeout) {
146
+ const describeResult = await client.send(
147
+ new DescribeTableCommand({ TableName: tableName }),
148
+ ) as { Table?: { TableStatus?: string, TableArn?: string } }
149
+
150
+ if (describeResult.Table?.TableStatus === 'ACTIVE') {
151
+ return {
152
+ success: true,
153
+ tableName,
154
+ tableArn: describeResult.Table.TableArn,
155
+ }
156
+ }
157
+
158
+ // Wait 1 second before checking again
159
+ await new Promise(resolve => setTimeout(resolve, 1000))
160
+ }
161
+
162
+ return {
163
+ success: false,
164
+ tableName,
165
+ error: 'Timeout waiting for table to become active',
166
+ }
167
+ }
168
+
169
+ return {
170
+ success: true,
171
+ tableName,
172
+ }
173
+ }
174
+ catch (err) {
175
+ return {
176
+ success: false,
177
+ tableName,
178
+ error: err instanceof Error ? err.message : String(err),
179
+ }
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Enable TTL on the analytics table
185
+ */
186
+ export async function enableTtl(
187
+ client: DynamoDBClientLike,
188
+ tableName: string,
189
+ attributeName: string,
190
+ commandFactory: {
191
+ UpdateTimeToLiveCommand: new (input: {
192
+ TableName: string
193
+ TimeToLiveSpecification: { AttributeName: string, Enabled: boolean }
194
+ }) => unknown
195
+ },
196
+ ): Promise<SetupResult> {
197
+ const { UpdateTimeToLiveCommand } = commandFactory
198
+
199
+ try {
200
+ await client.send(
201
+ new UpdateTimeToLiveCommand({
202
+ TableName: tableName,
203
+ TimeToLiveSpecification: {
204
+ AttributeName: attributeName,
205
+ Enabled: true,
206
+ },
207
+ }),
208
+ )
209
+
210
+ return {
211
+ success: true,
212
+ tableName,
213
+ }
214
+ }
215
+ catch (err) {
216
+ return {
217
+ success: false,
218
+ tableName,
219
+ error: err instanceof Error ? err.message : String(err),
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Check if table exists and is active
226
+ */
227
+ // eslint-disable-next-line pickier/no-unused-vars
228
+ export async function checkTableStatus(
229
+ client: DynamoDBClientLike,
230
+ tableName: string,
231
+ commandFactory: {
232
+ DescribeTableCommand: new (input: { TableName: string }) => unknown
233
+ },
234
+ ): Promise<{
235
+ exists: boolean
236
+ status?: string
237
+ itemCount?: number
238
+ sizeBytes?: number
239
+ gsiCount?: number
240
+ }> {
241
+ const { DescribeTableCommand } = commandFactory
242
+
243
+ try {
244
+ const result = await client.send(
245
+ new DescribeTableCommand({ TableName: tableName }),
246
+ ) as {
247
+ Table?: {
248
+ TableStatus?: string
249
+ ItemCount?: number
250
+ TableSizeBytes?: number
251
+ GlobalSecondaryIndexes?: unknown[]
252
+ }
253
+ }
254
+
255
+ return {
256
+ exists: true,
257
+ status: result.Table?.TableStatus,
258
+ itemCount: result.Table?.ItemCount,
259
+ sizeBytes: result.Table?.TableSizeBytes,
260
+ gsiCount: result.Table?.GlobalSecondaryIndexes?.length,
261
+ }
262
+ }
263
+ catch (err) {
264
+ if ((err as { name?: string }).name === 'ResourceNotFoundException') {
265
+ return { exists: false }
266
+ }
267
+ throw err
268
+ }
269
+ }
270
+
271
+ // ============================================================================
272
+ // Migration Functions
273
+ // ============================================================================
274
+
275
+ /**
276
+ * Run analytics migrations
277
+ */
278
+ export async function runMigrations(
279
+ client: DynamoDBClientLike,
280
+ _tableName: string,
281
+ migrations: MigrationStep[],
282
+ ): Promise<MigrationResult> {
283
+ const stepsCompleted: string[] = []
284
+
285
+ for (const migration of migrations) {
286
+ try {
287
+ await migration.execute(client)
288
+ stepsCompleted.push(migration.name)
289
+ }
290
+ catch (err) {
291
+ // Attempt rollback of completed steps
292
+ for (const completedName of stepsCompleted.reverse()) {
293
+ const completedMigration = migrations.find(m => m.name === completedName)
294
+ if (completedMigration?.rollback) {
295
+ try {
296
+ await completedMigration.rollback(client)
297
+ }
298
+ catch {
299
+ // Ignore rollback errors
300
+ }
301
+ }
302
+ }
303
+
304
+ return {
305
+ success: false,
306
+ stepsCompleted,
307
+ error: err instanceof Error ? err.message : String(err),
308
+ failedStep: migration.name,
309
+ }
310
+ }
311
+ }
312
+
313
+ return {
314
+ success: true,
315
+ stepsCompleted,
316
+ }
317
+ }
318
+
319
+ // ============================================================================
320
+ // Pre-defined Migrations
321
+ // ============================================================================
322
+
323
+ /**
324
+ * Migration: Add GSI for time-based queries
325
+ */
326
+ export function createTimeBasedGsiMigration(
327
+ tableName: string,
328
+ commandFactory: {
329
+ UpdateTableCommand: new (input: Record<string, unknown>) => unknown
330
+ },
331
+ ): MigrationStep {
332
+ const { UpdateTableCommand } = commandFactory
333
+
334
+ return {
335
+ name: 'add-time-gsi',
336
+ description: 'Add GSI for time-based queries (gsi3pk/gsi3sk)',
337
+ execute: async (client) => {
338
+ await client.send(
339
+ new UpdateTableCommand({
340
+ TableName: tableName,
341
+ AttributeDefinitions: [
342
+ { AttributeName: 'gsi3pk', AttributeType: 'S' },
343
+ { AttributeName: 'gsi3sk', AttributeType: 'S' },
344
+ ],
345
+ GlobalSecondaryIndexUpdates: [
346
+ {
347
+ Create: {
348
+ IndexName: 'GSI3',
349
+ KeySchema: [
350
+ { AttributeName: 'gsi3pk', KeyType: 'HASH' },
351
+ { AttributeName: 'gsi3sk', KeyType: 'RANGE' },
352
+ ],
353
+ Projection: { ProjectionType: 'ALL' },
354
+ },
355
+ },
356
+ ],
357
+ }),
358
+ )
359
+ },
360
+ rollback: async (client) => {
361
+ await client.send(
362
+ new UpdateTableCommand({
363
+ TableName: tableName,
364
+ GlobalSecondaryIndexUpdates: [
365
+ {
366
+ Delete: { IndexName: 'GSI3' },
367
+ },
368
+ ],
369
+ }),
370
+ )
371
+ },
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Migration: Enable Point-in-Time Recovery
377
+ */
378
+ export function createPitrMigration(
379
+ tableName: string,
380
+ commandFactory: {
381
+ UpdateContinuousBackupsCommand: new (input: Record<string, unknown>) => unknown
382
+ },
383
+ ): MigrationStep {
384
+ const { UpdateContinuousBackupsCommand } = commandFactory
385
+
386
+ return {
387
+ name: 'enable-pitr',
388
+ description: 'Enable Point-in-Time Recovery',
389
+ execute: async (client) => {
390
+ await client.send(
391
+ new UpdateContinuousBackupsCommand({
392
+ TableName: tableName,
393
+ PointInTimeRecoverySpecification: {
394
+ PointInTimeRecoveryEnabled: true,
395
+ },
396
+ }),
397
+ )
398
+ },
399
+ rollback: async (client) => {
400
+ await client.send(
401
+ new UpdateContinuousBackupsCommand({
402
+ TableName: tableName,
403
+ PointInTimeRecoverySpecification: {
404
+ PointInTimeRecoveryEnabled: false,
405
+ },
406
+ }),
407
+ )
408
+ },
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Migration: Enable DynamoDB Streams
414
+ */
415
+ export function createStreamsMigration(
416
+ tableName: string,
417
+ viewType: 'KEYS_ONLY' | 'NEW_IMAGE' | 'OLD_IMAGE' | 'NEW_AND_OLD_IMAGES',
418
+ commandFactory: {
419
+ UpdateTableCommand: new (input: Record<string, unknown>) => unknown
420
+ },
421
+ ): MigrationStep {
422
+ const { UpdateTableCommand } = commandFactory
423
+
424
+ return {
425
+ name: 'enable-streams',
426
+ description: `Enable DynamoDB Streams (${viewType})`,
427
+ execute: async (client) => {
428
+ await client.send(
429
+ new UpdateTableCommand({
430
+ TableName: tableName,
431
+ StreamSpecification: {
432
+ StreamEnabled: true,
433
+ StreamViewType: viewType,
434
+ },
435
+ }),
436
+ )
437
+ },
438
+ rollback: async (client) => {
439
+ await client.send(
440
+ new UpdateTableCommand({
441
+ TableName: tableName,
442
+ StreamSpecification: {
443
+ StreamEnabled: false,
444
+ },
445
+ }),
446
+ )
447
+ },
448
+ }
449
+ }
450
+
451
+ // ============================================================================
452
+ // CLI Helper
453
+ // ============================================================================
454
+
455
+ /**
456
+ * Print setup instructions for manual deployment
457
+ */
458
+ export function printSetupInstructions(config: SetupConfig = {}): void {
459
+ const tableName = config.tableName || 'analytics'
460
+
461
+ console.log(`
462
+ # Analytics DynamoDB Table Setup
463
+
464
+ ## Option 1: AWS CLI
465
+
466
+ \`\`\`bash
467
+ aws dynamodb create-table \\
468
+ --table-name ${tableName} \\
469
+ --attribute-definitions \\
470
+ AttributeName=pk,AttributeType=S \\
471
+ AttributeName=sk,AttributeType=S \\
472
+ AttributeName=gsi1pk,AttributeType=S \\
473
+ AttributeName=gsi1sk,AttributeType=S \\
474
+ AttributeName=gsi2pk,AttributeType=S \\
475
+ AttributeName=gsi2sk,AttributeType=S \\
476
+ --key-schema \\
477
+ AttributeName=pk,KeyType=HASH \\
478
+ AttributeName=sk,KeyType=RANGE \\
479
+ --billing-mode PAY_PER_REQUEST \\
480
+ --global-secondary-indexes \\
481
+ '[
482
+ {
483
+ "IndexName": "GSI1",
484
+ "KeySchema": [
485
+ {"AttributeName": "gsi1pk", "KeyType": "HASH"},
486
+ {"AttributeName": "gsi1sk", "KeyType": "RANGE"}
487
+ ],
488
+ "Projection": {"ProjectionType": "ALL"}
489
+ },
490
+ {
491
+ "IndexName": "GSI2",
492
+ "KeySchema": [
493
+ {"AttributeName": "gsi2pk", "KeyType": "HASH"},
494
+ {"AttributeName": "gsi2sk", "KeyType": "RANGE"}
495
+ ],
496
+ "Projection": {"ProjectionType": "ALL"}
497
+ }
498
+ ]'
499
+
500
+ # Enable TTL
501
+ aws dynamodb update-time-to-live \\
502
+ --table-name ${tableName} \\
503
+ --time-to-live-specification AttributeName=ttl,Enabled=true
504
+
505
+ # Enable PITR
506
+ aws dynamodb update-continuous-backups \\
507
+ --table-name ${tableName} \\
508
+ --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
509
+ \`\`\`
510
+
511
+ ## Option 2: CloudFormation
512
+
513
+ \`\`\`bash
514
+ # Generate template
515
+ npx dynamodb-tooling analytics:cfn > analytics.yaml
516
+
517
+ # Deploy
518
+ aws cloudformation deploy \\
519
+ --template-file analytics.yaml \\
520
+ --stack-name ${tableName}-stack
521
+ \`\`\`
522
+
523
+ ## Option 3: CDK
524
+
525
+ \`\`\`bash
526
+ # Generate CDK code
527
+ npx dynamodb-tooling analytics:cdk > lib/analytics-stack.ts
528
+
529
+ # Deploy
530
+ cdk deploy AnalyticsStack
531
+ \`\`\`
532
+
533
+ ## Option 4: SAM
534
+
535
+ \`\`\`bash
536
+ # Generate SAM template
537
+ npx dynamodb-tooling analytics:sam > template.yaml
538
+
539
+ # Deploy
540
+ sam build && sam deploy --guided
541
+ \`\`\`
542
+ `)
543
+ }
544
+
545
+ /**
546
+ * Generate AWS CLI commands for table creation
547
+ */
548
+ export function generateAwsCliCommands(config: SetupConfig = {}): string {
549
+ const tableName = config.tableName || 'analytics'
550
+ const region = config.region || 'us-east-1'
551
+
552
+ return `#!/bin/bash
553
+ set -e
554
+
555
+ TABLE_NAME="${tableName}"
556
+ REGION="${region}"
557
+
558
+ echo "Creating analytics table: $TABLE_NAME"
559
+
560
+ # Create table
561
+ aws dynamodb create-table \\
562
+ --table-name $TABLE_NAME \\
563
+ --region $REGION \\
564
+ --attribute-definitions \\
565
+ AttributeName=pk,AttributeType=S \\
566
+ AttributeName=sk,AttributeType=S \\
567
+ AttributeName=gsi1pk,AttributeType=S \\
568
+ AttributeName=gsi1sk,AttributeType=S \\
569
+ AttributeName=gsi2pk,AttributeType=S \\
570
+ AttributeName=gsi2sk,AttributeType=S \\
571
+ --key-schema \\
572
+ AttributeName=pk,KeyType=HASH \\
573
+ AttributeName=sk,KeyType=RANGE \\
574
+ --billing-mode PAY_PER_REQUEST \\
575
+ --global-secondary-indexes '[
576
+ {
577
+ "IndexName": "GSI1",
578
+ "KeySchema": [
579
+ {"AttributeName": "gsi1pk", "KeyType": "HASH"},
580
+ {"AttributeName": "gsi1sk", "KeyType": "RANGE"}
581
+ ],
582
+ "Projection": {"ProjectionType": "ALL"}
583
+ },
584
+ {
585
+ "IndexName": "GSI2",
586
+ "KeySchema": [
587
+ {"AttributeName": "gsi2pk", "KeyType": "HASH"},
588
+ {"AttributeName": "gsi2sk", "KeyType": "RANGE"}
589
+ ],
590
+ "Projection": {"ProjectionType": "ALL"}
591
+ }
592
+ ]'
593
+
594
+ echo "Waiting for table to be active..."
595
+ aws dynamodb wait table-exists --table-name $TABLE_NAME --region $REGION
596
+
597
+ echo "Enabling TTL..."
598
+ aws dynamodb update-time-to-live \\
599
+ --table-name $TABLE_NAME \\
600
+ --region $REGION \\
601
+ --time-to-live-specification AttributeName=ttl,Enabled=true
602
+
603
+ echo "Enabling Point-in-Time Recovery..."
604
+ aws dynamodb update-continuous-backups \\
605
+ --table-name $TABLE_NAME \\
606
+ --region $REGION \\
607
+ --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
608
+
609
+ echo "Done! Table $TABLE_NAME is ready."
610
+ `
611
+ }