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.
- package/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- 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();
|