lytx 0.3.0

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 (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
@@ -0,0 +1,518 @@
1
+ #!/usr/bin/env bun
2
+ import { createId } from "@paralleldrive/cuid2";
3
+ import { execSync } from "child_process";
4
+ import { writeFileSync, unlinkSync } from "fs";
5
+ import { join } from "path";
6
+ import { drizzle } from "drizzle-orm/postgres-js";
7
+ import postgres from "postgres";
8
+ import { sites as pgSites } from "@db/postgres/schema";
9
+ import { eq, and } from "drizzle-orm";
10
+
11
+ // Parse CLI arguments
12
+ const args = process.argv.slice(2);
13
+ const getArg = (flag: string, defaultValue?: string): string => {
14
+ const index = args.indexOf(flag);
15
+ if (index === -1) {
16
+ if (defaultValue !== undefined) return defaultValue;
17
+ throw new Error(`Missing required argument: ${flag}`);
18
+ }
19
+ const value = args[index + 1];
20
+ if (!value || value.startsWith("-")) {
21
+ throw new Error(`Invalid value for ${flag}`);
22
+ }
23
+ return value;
24
+ };
25
+
26
+ const hasFlag = (flag: string): boolean => args.includes(flag);
27
+
28
+ // Check for help flag first
29
+ if (hasFlag("--help") || hasFlag("-h")) {
30
+ console.log(`
31
+ Usage: bun run cli/import-sites.ts [options]
32
+
33
+ Options:
34
+ -t, --team-id <id> Team ID to import sites for (required)
35
+ -d, --database <name> Database name (default: "lytx_core_db")
36
+ --local Use local database (default: true)
37
+ --remote Use remote database (default: false)
38
+ --from-db <url> Import from PostgreSQL database (provide connection string)
39
+ --from-account-id <id> Account ID to import from (when using --from-db)
40
+ --remote-site-id <id> Specific site ID to import from remote DB (optional filter)
41
+ -h, --help Show this help message
42
+
43
+ Site Data Sources:
44
+ 1. JSON via stdin (default):
45
+ The script expects JSON data piped to stdin with the following structure:
46
+ [
47
+ {
48
+ "name": "Site Name",
49
+ "domain": "example.com",
50
+ "track_web_events": true,
51
+ "gdpr": false,
52
+ "event_load_strategy": "sdk"
53
+ },
54
+ ...
55
+ ]
56
+
57
+ Required fields:
58
+ - name: Site name (string)
59
+ - domain: Site domain (string)
60
+
61
+ Optional fields:
62
+ - track_web_events: Enable web event tracking (boolean, default: true)
63
+ - gdpr: GDPR compliance mode (boolean, default: false)
64
+ - event_load_strategy: "sdk" to skip KV events (default: "sdk")
65
+
66
+ 2. PostgreSQL database:
67
+ Use --from-db with a connection string to import from another database
68
+
69
+ Examples:
70
+ # Import from JSON stdin
71
+ echo '[{"name":"My Site","domain":"example.com"}]' | bun run cli/import-sites.ts --team-id 1
72
+ cat sites.json | bun run cli/import-sites.ts --team-id 1 --remote
73
+ bun run cli/import-sites.ts --team-id 1 --local < sites.json
74
+
75
+ # Import from PostgreSQL database
76
+ bun run cli/import-sites.ts --team-id 1 --from-db "postgresql://user:pass@host:5432/db" --from-account-id 5
77
+ bun run cli/import-sites.ts --team-id 1 --from-db "$DATABASE_URL" --from-account-id 5 --remote
78
+
79
+ # Import specific site from PostgreSQL database
80
+ bun run cli/import-sites.ts --team-id 6 --from-db "$DATABASE_URL" --from-account-id 4 --remote-site-id 55 --remote
81
+ `);
82
+ process.exit(0);
83
+ }
84
+
85
+ // CLI argument parsing
86
+ const getTeamIdArg = () => {
87
+ try {
88
+ return parseInt(getArg("--team-id"));
89
+ } catch {
90
+ return parseInt(getArg("-t"));
91
+ }
92
+ };
93
+
94
+ const getDatabaseArg = () => {
95
+ try {
96
+ return getArg("--database");
97
+ } catch {
98
+ try {
99
+ return getArg("-d");
100
+ } catch {
101
+ return "lytx_core_db";
102
+ }
103
+ }
104
+ };
105
+
106
+ const getFromDbArg = () => {
107
+ try {
108
+ return getArg("--from-db");
109
+ } catch {
110
+ return null;
111
+ }
112
+ };
113
+
114
+ const getFromAccountIdArg = () => {
115
+ try {
116
+ return parseInt(getArg("--from-account-id"));
117
+ } catch {
118
+ return null;
119
+ }
120
+ };
121
+
122
+ const getRemoteSiteIdArg = () => {
123
+ try {
124
+ return parseInt(getArg("--remote-site-id"));
125
+ } catch {
126
+ return null;
127
+ }
128
+ };
129
+
130
+ type SiteInput = {
131
+ name: string;
132
+ domain: string;
133
+ track_web_events?: boolean;
134
+ gdpr?: boolean;
135
+ event_load_strategy?: "sdk" | "kv";
136
+ };
137
+
138
+ async function readStdin(): Promise<string> {
139
+ return new Promise((resolve, reject) => {
140
+ let data = "";
141
+ process.stdin.setEncoding("utf8");
142
+ process.stdin.on("data", (chunk: string) => {
143
+ data += chunk;
144
+ });
145
+ process.stdin.on("end", () => resolve(data));
146
+ process.stdin.on("error", (error: Error) => reject(error));
147
+ });
148
+ }
149
+
150
+ function validateSiteData(input: unknown): SiteInput[] {
151
+ if (!Array.isArray(input)) {
152
+ throw new Error("Site data must be an array of sites");
153
+ }
154
+
155
+ return input.map((site, index) => {
156
+ if (!site || typeof site !== "object") {
157
+ throw new Error(`Site at index ${index} must be an object`);
158
+ }
159
+
160
+ const parsed = site as SiteInput;
161
+ if (!parsed.name || typeof parsed.name !== "string") {
162
+ throw new Error(`Site at index ${index} is missing a valid name`);
163
+ }
164
+ if (!parsed.domain || typeof parsed.domain !== "string") {
165
+ throw new Error(`Site at index ${index} is missing a valid domain`);
166
+ }
167
+
168
+ if (parsed.track_web_events !== undefined && typeof parsed.track_web_events !== "boolean") {
169
+ throw new Error(`Site at index ${index}: 'track_web_events' must be a boolean`);
170
+ }
171
+ if (parsed.gdpr !== undefined && typeof parsed.gdpr !== "boolean") {
172
+ throw new Error(`Site at index ${index}: 'gdpr' must be a boolean`);
173
+ }
174
+ if (
175
+ parsed.event_load_strategy !== undefined &&
176
+ parsed.event_load_strategy !== "sdk" &&
177
+ parsed.event_load_strategy !== "kv"
178
+ ) {
179
+ throw new Error(`Site at index ${index}: 'event_load_strategy' must be "sdk" or "kv"`);
180
+ }
181
+
182
+ return {
183
+ name: parsed.name,
184
+ domain: parsed.domain,
185
+ track_web_events: parsed.track_web_events ?? true,
186
+ gdpr: parsed.gdpr ?? false,
187
+ event_load_strategy: parsed.event_load_strategy ?? "sdk",
188
+ };
189
+ });
190
+ }
191
+
192
+ const teamId = getTeamIdArg();
193
+ const database = getDatabaseArg();
194
+ const fromDb = getFromDbArg();
195
+ const fromAccountId = getFromAccountIdArg();
196
+ const remoteSiteId = getRemoteSiteIdArg();
197
+ const isRemote = hasFlag("--remote");
198
+ const isLocal = hasFlag("--local") || !isRemote;
199
+
200
+ // Helper function to execute SQL via wrangler
201
+ function executeSQL(sql: string, description: string) {
202
+ console.log(`šŸ“ ${description}...`);
203
+
204
+ const tempFile = join(process.cwd(), `temp_${Date.now()}.sql`);
205
+ writeFileSync(tempFile, sql);
206
+
207
+ try {
208
+ const command = `bunx wrangler d1 execute ${database} --file ${tempFile} ${isLocal ? "--local" : "--remote"} --yes`;
209
+ const result = execSync(command, { encoding: "utf8", stdio: "pipe" });
210
+ console.log(`āœ… ${description} completed`);
211
+ return result;
212
+ } catch (error: any) {
213
+ console.error(`āŒ Error during ${description}:`, error.message);
214
+ throw error;
215
+ } finally {
216
+ try {
217
+ unlinkSync(tempFile);
218
+ } catch (e) {
219
+ // Ignore cleanup errors
220
+ }
221
+ }
222
+ }
223
+
224
+ // Helper function to validate team exists
225
+ async function validateTeam(teamId: number): Promise<void> {
226
+ const validateTeamSQL = `SELECT id, name FROM team WHERE id = ${teamId};`;
227
+ const tempFile = join(process.cwd(), `temp_query_${Date.now()}.sql`);
228
+ writeFileSync(tempFile, validateTeamSQL);
229
+
230
+ try {
231
+ const command = `bunx wrangler d1 execute ${database} --file ${tempFile} ${isLocal ? "--local" : "--remote"} --json --yes`;
232
+ const result = execSync(command, { encoding: "utf8", stdio: "pipe" });
233
+
234
+ // Extract JSON from wrangler output (skip progress indicators)
235
+ const jsonStart = result.indexOf("[");
236
+ const jsonEnd = result.lastIndexOf("]") + 1;
237
+
238
+ if (jsonStart === -1 || jsonEnd === 0) {
239
+ console.error("āŒ No JSON found in wrangler output:");
240
+ console.error("Raw output:", result);
241
+ throw new Error(
242
+ `Wrangler did not return valid JSON. Check database connection and permissions.`,
243
+ );
244
+ }
245
+
246
+ const jsonString = result.substring(jsonStart, jsonEnd);
247
+
248
+ let jsonResult;
249
+ try {
250
+ jsonResult = JSON.parse(jsonString);
251
+ } catch (parseError) {
252
+ console.error("āŒ Failed to parse extracted JSON:");
253
+ console.error("Extracted JSON:", jsonString);
254
+ throw new Error(`Invalid JSON format from wrangler.`, { cause: parseError });
255
+ }
256
+
257
+ if (!jsonResult || !jsonResult[0] || !jsonResult[0].results) {
258
+ throw new Error(
259
+ `Unexpected response format from wrangler: ${JSON.stringify(jsonResult)}`,
260
+ );
261
+ }
262
+
263
+ if (jsonResult[0].results.length === 0) {
264
+ throw new Error(`Team ID ${teamId} not found`);
265
+ }
266
+
267
+ const team = jsonResult[0].results[0];
268
+ console.log(
269
+ `āœ… Found team: ${team.name || "Unknown"} (ID: ${team.id || teamId})`,
270
+ );
271
+ } catch (error: any) {
272
+ console.error("āŒ Error validating team:", error.message);
273
+ throw error;
274
+ } finally {
275
+ try {
276
+ unlinkSync(tempFile);
277
+ } catch (e) {
278
+ // Ignore cleanup errors
279
+ }
280
+ }
281
+ }
282
+
283
+ // Helper function to read from PostgreSQL database
284
+ async function readFromDatabase(
285
+ connectionString: string,
286
+ accountId: number,
287
+ siteId?: number,
288
+ ): Promise<SiteInput[]> {
289
+ console.log("šŸ“– Reading sites from PostgreSQL database...");
290
+ if (siteId) {
291
+ console.log(`šŸŽÆ Filtering for specific site ID: ${siteId}`);
292
+ }
293
+
294
+ const sql = postgres(connectionString);
295
+ const db = drizzle(sql);
296
+
297
+ try {
298
+ // Build where conditions
299
+ const whereConditions = [eq(pgSites.account_id, accountId)];
300
+ if (siteId) {
301
+ whereConditions.push(eq(pgSites.site_id, siteId));
302
+ }
303
+
304
+ const sites = await db
305
+ .select({
306
+ site_id: pgSites.site_id,
307
+ tag_id: pgSites.tag_id,
308
+ domain: pgSites.domain,
309
+ track_web_events: pgSites.track_web_events,
310
+ gdpr: pgSites.gdpr,
311
+ event_load_strategy: pgSites.event_load_strategy ?? "sdk",
312
+ })
313
+ .from(pgSites)
314
+ .where(and(...whereConditions));
315
+
316
+ return sites.map((site) => ({
317
+ name: site.domain || `Site ${site.site_id}`,
318
+ domain: site.domain ?? "",
319
+ track_web_events: site.track_web_events ?? true,
320
+ gdpr: site.gdpr ?? false,
321
+ event_load_strategy: (site.event_load_strategy ?? "sdk") as "sdk" | "kv",
322
+ }));
323
+ } finally {
324
+ await sql.end();
325
+ }
326
+ }
327
+
328
+ async function importSites() {
329
+ try {
330
+ console.log("šŸš€ Starting site import...");
331
+ console.log(`šŸ“Š Target: ${database} (${isLocal ? "local" : "remote"})`);
332
+ console.log(`šŸ¢ Team ID: ${teamId}`);
333
+
334
+ // Validate team exists
335
+ await validateTeam(teamId);
336
+
337
+ // Get site data from either database or stdin
338
+ let sites: SiteInput[];
339
+
340
+ if (fromDb) {
341
+ if (!fromAccountId) {
342
+ throw new Error("--from-account-id is required when using --from-db");
343
+ }
344
+ console.log(
345
+ `šŸ“Š Source: PostgreSQL database (Account ID: ${fromAccountId}${remoteSiteId ? `, Site ID: ${remoteSiteId}` : ""})`,
346
+ );
347
+ sites = await readFromDatabase(
348
+ fromDb,
349
+ fromAccountId,
350
+ remoteSiteId || undefined,
351
+ );
352
+ } else {
353
+ // Read JSON data from stdin
354
+ console.log("šŸ“– Reading site data from stdin...");
355
+ const stdinData = await readStdin();
356
+
357
+ if (!stdinData) {
358
+ throw new Error(
359
+ "No data provided via stdin. Please pipe JSON data to this command.",
360
+ );
361
+ }
362
+
363
+ // Parse JSON
364
+ let sitesData: any;
365
+ try {
366
+ sitesData = JSON.parse(stdinData);
367
+ } catch (error: any) {
368
+ throw new Error(`Invalid JSON format: ${error.message}`, { cause: error });
369
+ }
370
+
371
+ // Validate site data
372
+ sites = validateSiteData(sitesData);
373
+ }
374
+ console.log(`šŸ“‹ Found ${sites.length} sites to import`);
375
+
376
+ if (sites.length === 0) {
377
+ console.log("ā„¹ļø No sites to import");
378
+ return;
379
+ }
380
+
381
+ // Import sites
382
+ const importedSites: Array<{
383
+ name: string;
384
+ domain: string;
385
+ tagId: string;
386
+ siteId?: number;
387
+ }> = [];
388
+
389
+ for (const [index, site] of sites.entries()) {
390
+ console.log(
391
+ `\nšŸ“¦ Importing site ${index + 1}/${sites.length}: ${site.name}`,
392
+ );
393
+
394
+ // Generate unique IDs
395
+ const tagId = createId();
396
+ const ridSalt = createId();
397
+ const now = Math.floor(Date.now() / 1000); // Unix timestamp in seconds
398
+ const ridSaltExpire = now + 30 * 24 * 60 * 60; // 30 days from now
399
+
400
+ // Escape single quotes in strings for SQL
401
+ const escapedName = site.name.replace(/'/g, "''");
402
+ const escapedDomain = site.domain.replace(/'/g, "''");
403
+
404
+ // Create site SQL
405
+ const siteSQL = `
406
+ INSERT INTO sites (tag_id, track_web_events, event_load_strategy, team_id, name, domain, gdpr, rid_salt, rid_salt_expire, created_at, updated_at)
407
+ VALUES ('${tagId}', ${site.track_web_events ? 1 : 0}, '${site.event_load_strategy ?? "sdk"}', ${teamId}, '${escapedName}', '${escapedDomain}', ${site.gdpr ? 1 : 0}, '${ridSalt}', ${ridSaltExpire}, ${now}, ${now});
408
+ `;
409
+
410
+ executeSQL(siteSQL, `Creating site: ${site.name}`);
411
+
412
+ // Get the created site ID
413
+ const getSiteIdSQL = `SELECT site_id FROM sites WHERE tag_id = '${tagId}';`;
414
+ const tempFile = join(process.cwd(), `temp_query_${Date.now()}.sql`);
415
+ writeFileSync(tempFile, getSiteIdSQL);
416
+
417
+ try {
418
+ const command = `bunx wrangler d1 execute ${database} --file ${tempFile} ${isLocal ? "--local" : "--remote"} --json --yes`;
419
+ const result = execSync(command, { encoding: "utf8", stdio: "pipe" });
420
+
421
+ // Extract JSON from wrangler output
422
+ const jsonStart = result.indexOf("[");
423
+ const jsonEnd = result.lastIndexOf("]") + 1;
424
+ const jsonString = result.substring(jsonStart, jsonEnd);
425
+ const jsonResult = JSON.parse(jsonString);
426
+ const siteId =
427
+ jsonResult[0].results[0].site_id || jsonResult[0].results[0].id;
428
+
429
+ importedSites.push({
430
+ name: site.name,
431
+ domain: site.domain,
432
+ tagId,
433
+ siteId,
434
+ });
435
+
436
+ console.log(`āœ… Created site ID: ${siteId} for ${site.name}`);
437
+ } catch (error: any) {
438
+ console.error(
439
+ `āŒ Error getting site ID for ${site.name}:`,
440
+ error.message,
441
+ );
442
+ // Add to imported sites without siteId for reporting
443
+ importedSites.push({
444
+ name: site.name,
445
+ domain: site.domain,
446
+ tagId,
447
+ });
448
+ } finally {
449
+ try {
450
+ unlinkSync(tempFile);
451
+ } catch (e) {
452
+ // Ignore cleanup errors
453
+ }
454
+ }
455
+ }
456
+
457
+ console.log("\nāœ… Site import complete!");
458
+ console.log(`
459
+ šŸ“Š Summary:
460
+ Sites imported: ${importedSites.length}
461
+ Team ID: ${teamId}
462
+ Database: ${database} (${isLocal ? "local" : "remote"})
463
+ `);
464
+
465
+ console.log(`
466
+ 🌐 Imported sites:
467
+ ${importedSites
468
+ .map(
469
+ (site) =>
470
+ ` - ${site.name} (${site.domain})${site.siteId ? ` - Site ID: ${site.siteId}` : ""} - Tag ID: ${site.tagId}`,
471
+ )
472
+ .join("\n")}
473
+ `);
474
+
475
+ console.log(`
476
+ šŸš€ Next steps:
477
+ 1. Start the dev server: bun run dev
478
+ 2. Login and verify the sites appear in your dashboard
479
+ 3. Use the Tag IDs above to implement tracking on your sites
480
+ 4. Optionally run seed-data.ts to generate sample events for testing
481
+ `);
482
+ } catch (error) {
483
+ console.error("āŒ Error importing sites:", error);
484
+ process.exit(1);
485
+ }
486
+ }
487
+
488
+ // Validate required arguments
489
+ if (!teamId || isNaN(teamId)) {
490
+ console.error("āŒ Error: --team-id is required and must be a number");
491
+ console.log("Use --help for usage information");
492
+ process.exit(1);
493
+ }
494
+
495
+ // Validate data source
496
+ if (fromDb) {
497
+ if (!fromAccountId || isNaN(fromAccountId)) {
498
+ console.error(
499
+ "āŒ Error: --from-account-id is required and must be a number when using --from-db",
500
+ );
501
+ console.log("Use --help for usage information");
502
+ process.exit(1);
503
+ }
504
+ } else {
505
+ // Check if stdin has data (when not run interactively)
506
+ if (process.stdin.isTTY) {
507
+ console.error(
508
+ "āŒ Error: No data provided via stdin and --from-db not specified",
509
+ );
510
+ console.log(
511
+ "Please pipe JSON data to this command or use --from-db option. Use --help for examples.",
512
+ );
513
+ process.exit(1);
514
+ }
515
+ }
516
+
517
+ // Run the import
518
+ importSites();