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,278 @@
|
|
|
1
|
+
import { prefix, route } from "rwsdk/router";
|
|
2
|
+
import { d1_client } from "@db/d1/client";
|
|
3
|
+
import { customReports } from "@db/d1/schema";
|
|
4
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
5
|
+
import { getSiteFromContext } from "@/api/authMiddleware";
|
|
6
|
+
|
|
7
|
+
const parseSiteId = (value: unknown) => {
|
|
8
|
+
const numeric = Number(value);
|
|
9
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return null;
|
|
10
|
+
return Math.floor(numeric);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const parseReportUuidFromPath = (requestUrl: string) => {
|
|
14
|
+
const pathname = new URL(requestUrl).pathname;
|
|
15
|
+
const marker = "/api/reports/custom/";
|
|
16
|
+
const markerIndex = pathname.indexOf(marker);
|
|
17
|
+
if (markerIndex < 0) return null;
|
|
18
|
+
|
|
19
|
+
const raw = pathname.slice(markerIndex + marker.length).trim();
|
|
20
|
+
if (!raw || raw.includes("/")) return null;
|
|
21
|
+
return decodeURIComponent(raw);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const listOrCreateCustomReports = route("/reports/custom", [
|
|
25
|
+
async ({ request, ctx }) => {
|
|
26
|
+
if (request.method === "GET") {
|
|
27
|
+
const url = new URL(request.url);
|
|
28
|
+
const siteIdParam = url.searchParams.get("site_id");
|
|
29
|
+
const parsedSiteId = siteIdParam ? parseSiteId(siteIdParam) : null;
|
|
30
|
+
|
|
31
|
+
if (siteIdParam && !parsedSiteId) {
|
|
32
|
+
return new Response(JSON.stringify({ error: "site_id must be a valid number" }), {
|
|
33
|
+
status: 400,
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (parsedSiteId) {
|
|
39
|
+
const site = getSiteFromContext(ctx, parsedSiteId);
|
|
40
|
+
if (!site) {
|
|
41
|
+
return new Response(JSON.stringify({ error: "Site not found" }), {
|
|
42
|
+
status: 404,
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const reports = parsedSiteId
|
|
49
|
+
? await d1_client
|
|
50
|
+
.select()
|
|
51
|
+
.from(customReports)
|
|
52
|
+
.where(and(eq(customReports.team_id, ctx.team.id), eq(customReports.site_id, parsedSiteId)))
|
|
53
|
+
.orderBy(desc(customReports.updatedAt))
|
|
54
|
+
: await d1_client
|
|
55
|
+
.select()
|
|
56
|
+
.from(customReports)
|
|
57
|
+
.where(eq(customReports.team_id, ctx.team.id))
|
|
58
|
+
.orderBy(desc(customReports.updatedAt));
|
|
59
|
+
|
|
60
|
+
return new Response(JSON.stringify({ reports }), {
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (request.method !== "POST") {
|
|
66
|
+
return new Response(JSON.stringify({ error: "Method not allowed" }), {
|
|
67
|
+
status: 405,
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (ctx.user_role === "viewer") {
|
|
73
|
+
return new Response(JSON.stringify({ error: "You need editor or admin permissions to create reports" }), {
|
|
74
|
+
status: 403,
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const payload = (await request.json().catch(() => null)) as
|
|
80
|
+
| {
|
|
81
|
+
site_id?: unknown;
|
|
82
|
+
name?: unknown;
|
|
83
|
+
description?: unknown;
|
|
84
|
+
config?: unknown;
|
|
85
|
+
}
|
|
86
|
+
| null;
|
|
87
|
+
|
|
88
|
+
const siteId = parseSiteId(payload?.site_id);
|
|
89
|
+
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
|
90
|
+
const description = typeof payload?.description === "string" ? payload.description.trim() : null;
|
|
91
|
+
const config = payload?.config;
|
|
92
|
+
|
|
93
|
+
if (!siteId) {
|
|
94
|
+
return new Response(JSON.stringify({ error: "site_id is required" }), {
|
|
95
|
+
status: 400,
|
|
96
|
+
headers: { "Content-Type": "application/json" },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!name) {
|
|
101
|
+
return new Response(JSON.stringify({ error: "name is required" }), {
|
|
102
|
+
status: 400,
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!config || typeof config !== "object") {
|
|
108
|
+
return new Response(JSON.stringify({ error: "config is required" }), {
|
|
109
|
+
status: 400,
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const site = getSiteFromContext(ctx, siteId);
|
|
115
|
+
if (!site) {
|
|
116
|
+
return new Response(JSON.stringify({ error: "Site not found" }), {
|
|
117
|
+
status: 404,
|
|
118
|
+
headers: { "Content-Type": "application/json" },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const uuid = crypto.randomUUID();
|
|
123
|
+
const userId = ((ctx.session as { user?: { id?: string } })?.user?.id || "unknown").toString();
|
|
124
|
+
|
|
125
|
+
await d1_client.insert(customReports).values({
|
|
126
|
+
uuid,
|
|
127
|
+
team_id: ctx.team.id,
|
|
128
|
+
site_id: siteId,
|
|
129
|
+
name,
|
|
130
|
+
description,
|
|
131
|
+
config: config as Record<string, unknown>,
|
|
132
|
+
created_by: userId,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return new Response(JSON.stringify({ uuid }), {
|
|
136
|
+
status: 201,
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
const getOrUpdateCustomReport = route("/reports/custom/*", [
|
|
143
|
+
async ({ request, ctx }) => {
|
|
144
|
+
const reportUuid = parseReportUuidFromPath(request.url);
|
|
145
|
+
if (!reportUuid) {
|
|
146
|
+
return new Response(JSON.stringify({ error: "Invalid report id" }), {
|
|
147
|
+
status: 400,
|
|
148
|
+
headers: { "Content-Type": "application/json" },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const existing = await d1_client
|
|
153
|
+
.select()
|
|
154
|
+
.from(customReports)
|
|
155
|
+
.where(and(eq(customReports.uuid, reportUuid), eq(customReports.team_id, ctx.team.id)))
|
|
156
|
+
.limit(1);
|
|
157
|
+
|
|
158
|
+
if (existing.length === 0) {
|
|
159
|
+
return new Response(JSON.stringify({ error: "Report not found" }), {
|
|
160
|
+
status: 404,
|
|
161
|
+
headers: { "Content-Type": "application/json" },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const report = existing[0];
|
|
166
|
+
if (request.method !== "DELETE") {
|
|
167
|
+
const site = getSiteFromContext(ctx, report.site_id);
|
|
168
|
+
if (!site) {
|
|
169
|
+
return new Response(JSON.stringify({ error: "Site not found" }), {
|
|
170
|
+
status: 404,
|
|
171
|
+
headers: { "Content-Type": "application/json" },
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (request.method === "GET") {
|
|
177
|
+
return new Response(JSON.stringify({ report }), {
|
|
178
|
+
headers: { "Content-Type": "application/json" },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (request.method === "DELETE") {
|
|
183
|
+
if (ctx.user_role === "viewer") {
|
|
184
|
+
return new Response(JSON.stringify({ error: "You need editor or admin permissions to delete reports" }), {
|
|
185
|
+
status: 403,
|
|
186
|
+
headers: { "Content-Type": "application/json" },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await d1_client
|
|
191
|
+
.delete(customReports)
|
|
192
|
+
.where(eq(customReports.id, report.id));
|
|
193
|
+
|
|
194
|
+
return new Response(JSON.stringify({ uuid: reportUuid }), {
|
|
195
|
+
status: 200,
|
|
196
|
+
headers: { "Content-Type": "application/json" },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (request.method !== "POST") {
|
|
201
|
+
return new Response(JSON.stringify({ error: "Method not allowed" }), {
|
|
202
|
+
status: 405,
|
|
203
|
+
headers: { "Content-Type": "application/json" },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (ctx.user_role === "viewer") {
|
|
208
|
+
return new Response(JSON.stringify({ error: "You need editor or admin permissions to update reports" }), {
|
|
209
|
+
status: 403,
|
|
210
|
+
headers: { "Content-Type": "application/json" },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const payload = (await request.json().catch(() => null)) as
|
|
215
|
+
| {
|
|
216
|
+
site_id?: unknown;
|
|
217
|
+
name?: unknown;
|
|
218
|
+
description?: unknown;
|
|
219
|
+
config?: unknown;
|
|
220
|
+
}
|
|
221
|
+
| null;
|
|
222
|
+
|
|
223
|
+
const siteId = parseSiteId(payload?.site_id);
|
|
224
|
+
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
|
225
|
+
const description = typeof payload?.description === "string" ? payload.description.trim() : null;
|
|
226
|
+
const config = payload?.config;
|
|
227
|
+
|
|
228
|
+
if (!siteId) {
|
|
229
|
+
return new Response(JSON.stringify({ error: "site_id is required" }), {
|
|
230
|
+
status: 400,
|
|
231
|
+
headers: { "Content-Type": "application/json" },
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!name) {
|
|
236
|
+
return new Response(JSON.stringify({ error: "name is required" }), {
|
|
237
|
+
status: 400,
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!config || typeof config !== "object") {
|
|
243
|
+
return new Response(JSON.stringify({ error: "config is required" }), {
|
|
244
|
+
status: 400,
|
|
245
|
+
headers: { "Content-Type": "application/json" },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const targetSite = getSiteFromContext(ctx, siteId);
|
|
250
|
+
if (!targetSite) {
|
|
251
|
+
return new Response(JSON.stringify({ error: "Site not found" }), {
|
|
252
|
+
status: 404,
|
|
253
|
+
headers: { "Content-Type": "application/json" },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await d1_client
|
|
258
|
+
.update(customReports)
|
|
259
|
+
.set({
|
|
260
|
+
site_id: siteId,
|
|
261
|
+
name,
|
|
262
|
+
description,
|
|
263
|
+
config: config as Record<string, unknown>,
|
|
264
|
+
updatedAt: new Date(),
|
|
265
|
+
})
|
|
266
|
+
.where(eq(customReports.id, report.id));
|
|
267
|
+
|
|
268
|
+
return new Response(JSON.stringify({ uuid: reportUuid }), {
|
|
269
|
+
status: 200,
|
|
270
|
+
headers: { "Content-Type": "application/json" },
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
export const reportsApi = prefix("/", [
|
|
276
|
+
listOrCreateCustomReports,
|
|
277
|
+
getOrUpdateCustomReport,
|
|
278
|
+
]);
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// src/api/seed_api.ts
|
|
2
|
+
// Seed API endpoint for local development - handles site lookup, creation, and event seeding
|
|
3
|
+
|
|
4
|
+
import { route } from 'rwsdk/router';
|
|
5
|
+
import { env } from 'cloudflare:workers';
|
|
6
|
+
import { d1_client } from '@db/d1/client';
|
|
7
|
+
import { sites, team } from '@db/d1/schema';
|
|
8
|
+
import { eq, and } from 'drizzle-orm';
|
|
9
|
+
import { IS_DEV } from 'rwsdk/constants';
|
|
10
|
+
import { createId } from '@paralleldrive/cuid2';
|
|
11
|
+
import { insertSiteEvents } from '@db/adapter';
|
|
12
|
+
import type { SiteEventInput } from '@/session/siteSchema';
|
|
13
|
+
import type { DBAdapter } from '@db/types';
|
|
14
|
+
|
|
15
|
+
const SEED_DATA_SECRET = (env as { SEED_DATA_SECRET?: string }).SEED_DATA_SECRET;
|
|
16
|
+
const ENVIRONMENT = (env as { ENVIRONMENT?: string }).ENVIRONMENT;
|
|
17
|
+
|
|
18
|
+
function validateSeedSecret(request: Request): Response | null {
|
|
19
|
+
const seedSecretHeader = request.headers.get("x-seed-secret");
|
|
20
|
+
|
|
21
|
+
// Multiple layers of protection to ensure this never runs in production
|
|
22
|
+
// 1. Check IS_DEV from rwsdk (based on build mode)
|
|
23
|
+
if (!IS_DEV) {
|
|
24
|
+
return new Response("Seed endpoint disabled outside dev.", { status: 403 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Explicitly check ENVIRONMENT env var
|
|
28
|
+
if (ENVIRONMENT === "production" || ENVIRONMENT === "staging") {
|
|
29
|
+
return new Response("Seed endpoint disabled in production/staging.", { status: 403 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 3. Require SEED_DATA_SECRET to be configured
|
|
33
|
+
if (!SEED_DATA_SECRET) {
|
|
34
|
+
return new Response("SEED_DATA_SECRET is not configured.", { status: 500 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 4. Validate the secret matches
|
|
38
|
+
if (seedSecretHeader !== SEED_DATA_SECRET) {
|
|
39
|
+
return new Response("Unauthorized", { status: 401 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* GET /api/seed/site/:siteId?teamId=X
|
|
47
|
+
* Look up a site by ID and team
|
|
48
|
+
*/
|
|
49
|
+
async function getSite(siteId: number, teamId: number) {
|
|
50
|
+
const [siteDetails] = await d1_client
|
|
51
|
+
.select({
|
|
52
|
+
site_id: sites.site_id,
|
|
53
|
+
uuid: sites.uuid,
|
|
54
|
+
tag_id: sites.tag_id,
|
|
55
|
+
name: sites.name,
|
|
56
|
+
domain: sites.domain,
|
|
57
|
+
site_db_adapter: sites.site_db_adapter,
|
|
58
|
+
})
|
|
59
|
+
.from(sites)
|
|
60
|
+
.where(and(eq(sites.site_id, siteId), eq(sites.team_id, teamId)))
|
|
61
|
+
.limit(1);
|
|
62
|
+
|
|
63
|
+
return siteDetails;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* POST /api/seed/site
|
|
68
|
+
* Create a new site
|
|
69
|
+
*/
|
|
70
|
+
async function createSite(data: {
|
|
71
|
+
teamId: number;
|
|
72
|
+
name: string;
|
|
73
|
+
domain: string;
|
|
74
|
+
}) {
|
|
75
|
+
const tagId = createId();
|
|
76
|
+
const uuid = createId();
|
|
77
|
+
const ridSalt = createId();
|
|
78
|
+
const ridSaltExpire = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
|
79
|
+
|
|
80
|
+
await d1_client.insert(sites).values({
|
|
81
|
+
uuid,
|
|
82
|
+
tag_id: tagId,
|
|
83
|
+
track_web_events: true,
|
|
84
|
+
event_load_strategy: 'sdk',
|
|
85
|
+
team_id: data.teamId,
|
|
86
|
+
name: data.name,
|
|
87
|
+
domain: data.domain,
|
|
88
|
+
gdpr: false,
|
|
89
|
+
rid_salt: ridSalt,
|
|
90
|
+
rid_salt_expire: ridSaltExpire,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Fetch the created site to get the auto-generated site_id
|
|
94
|
+
const [createdSite] = await d1_client
|
|
95
|
+
.select({
|
|
96
|
+
site_id: sites.site_id,
|
|
97
|
+
uuid: sites.uuid,
|
|
98
|
+
tag_id: sites.tag_id,
|
|
99
|
+
name: sites.name,
|
|
100
|
+
domain: sites.domain,
|
|
101
|
+
site_db_adapter: sites.site_db_adapter,
|
|
102
|
+
})
|
|
103
|
+
.from(sites)
|
|
104
|
+
.where(eq(sites.tag_id, tagId))
|
|
105
|
+
.limit(1);
|
|
106
|
+
|
|
107
|
+
return createdSite;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* POST /api/seed/events/:siteId
|
|
112
|
+
* Insert events into a site's durable object
|
|
113
|
+
*/
|
|
114
|
+
async function seedEvents(
|
|
115
|
+
siteId: number,
|
|
116
|
+
siteUuid: string,
|
|
117
|
+
siteAdapter: DBAdapter,
|
|
118
|
+
events: SiteEventInput[],
|
|
119
|
+
) {
|
|
120
|
+
const normalizedEvents = events.map((event) => ({
|
|
121
|
+
...event,
|
|
122
|
+
createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
|
|
123
|
+
updatedAt: event.updatedAt ? new Date(event.updatedAt) : undefined,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
return await insertSiteEvents(siteId, siteUuid, normalizedEvents, siteAdapter);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* GET /api/seed/team/:teamId
|
|
131
|
+
* Check if a team exists
|
|
132
|
+
*/
|
|
133
|
+
async function getTeam(teamId: number) {
|
|
134
|
+
const [teamDetails] = await d1_client
|
|
135
|
+
.select({
|
|
136
|
+
id: team.id,
|
|
137
|
+
name: team.name,
|
|
138
|
+
db_adapter: team.db_adapter,
|
|
139
|
+
})
|
|
140
|
+
.from(team)
|
|
141
|
+
.where(eq(team.id, teamId))
|
|
142
|
+
.limit(1);
|
|
143
|
+
|
|
144
|
+
return teamDetails;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Seed API endpoint
|
|
149
|
+
*
|
|
150
|
+
* Routes:
|
|
151
|
+
* - GET /api/seed/team/:teamId - Check if team exists
|
|
152
|
+
* - GET /api/seed/site/:siteId?teamId=X - Get site details
|
|
153
|
+
* - POST /api/seed/site - Create a new site
|
|
154
|
+
* - POST /api/seed/events/:siteId - Insert events into site
|
|
155
|
+
*/
|
|
156
|
+
export const seedApi = route(
|
|
157
|
+
"/api/seed/*",
|
|
158
|
+
async ({ params, request }) => {
|
|
159
|
+
// Validate seed secret for all requests
|
|
160
|
+
const authError = validateSeedSecret(request);
|
|
161
|
+
if (authError) return authError;
|
|
162
|
+
|
|
163
|
+
const pathPart = params.$0 ?? "";
|
|
164
|
+
const segments = pathPart.split("/").filter(Boolean);
|
|
165
|
+
const url = new URL(request.url);
|
|
166
|
+
|
|
167
|
+
// GET /api/seed/team/:teamId
|
|
168
|
+
if (request.method === "GET" && segments[0] === "team" && segments[1]) {
|
|
169
|
+
const teamId = parseInt(segments[1], 10);
|
|
170
|
+
if (isNaN(teamId)) {
|
|
171
|
+
return new Response("Invalid team ID", { status: 400 });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const teamDetails = await getTeam(teamId);
|
|
175
|
+
if (!teamDetails) {
|
|
176
|
+
return new Response("Team not found", { status: 404 });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return Response.json(teamDetails);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// GET /api/seed/site/:siteId?teamId=X
|
|
183
|
+
if (request.method === "GET" && segments[0] === "site" && segments[1]) {
|
|
184
|
+
const siteId = parseInt(segments[1], 10);
|
|
185
|
+
const teamIdStr = url.searchParams.get("teamId");
|
|
186
|
+
|
|
187
|
+
if (isNaN(siteId)) {
|
|
188
|
+
return new Response("Invalid site ID", { status: 400 });
|
|
189
|
+
}
|
|
190
|
+
if (!teamIdStr) {
|
|
191
|
+
return new Response("teamId query param required", { status: 400 });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const teamId = parseInt(teamIdStr, 10);
|
|
195
|
+
if (isNaN(teamId)) {
|
|
196
|
+
return new Response("Invalid team ID", { status: 400 });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const siteDetails = await getSite(siteId, teamId);
|
|
200
|
+
if (!siteDetails) {
|
|
201
|
+
return new Response("Site not found", { status: 404 });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return Response.json(siteDetails);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// POST /api/seed/site
|
|
208
|
+
if (request.method === "POST" && segments[0] === "site" && !segments[1]) {
|
|
209
|
+
try {
|
|
210
|
+
const body = await request.json() as {
|
|
211
|
+
teamId: number;
|
|
212
|
+
name: string;
|
|
213
|
+
domain: string;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (!body.teamId || !body.name || !body.domain) {
|
|
217
|
+
return new Response("teamId, name, and domain are required", { status: 400 });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Verify team exists
|
|
221
|
+
const teamDetails = await getTeam(body.teamId);
|
|
222
|
+
if (!teamDetails) {
|
|
223
|
+
return new Response(`Team ${body.teamId} not found`, { status: 404 });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const site = await createSite(body);
|
|
227
|
+
return Response.json(site, { status: 201 });
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error("Error creating site:", error);
|
|
230
|
+
return new Response(
|
|
231
|
+
error instanceof Error ? error.message : "Failed to create site",
|
|
232
|
+
{ status: 500 }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// POST /api/seed/events/:siteId
|
|
238
|
+
if (request.method === "POST" && segments[0] === "events" && segments[1]) {
|
|
239
|
+
const siteId = parseInt(segments[1], 10);
|
|
240
|
+
const teamIdStr = url.searchParams.get("teamId");
|
|
241
|
+
|
|
242
|
+
if (isNaN(siteId)) {
|
|
243
|
+
return new Response("Invalid site ID", { status: 400 });
|
|
244
|
+
}
|
|
245
|
+
if (!teamIdStr) {
|
|
246
|
+
return new Response("teamId query param required", { status: 400 });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const teamId = parseInt(teamIdStr, 10);
|
|
250
|
+
if (isNaN(teamId)) {
|
|
251
|
+
return new Response("Invalid team ID", { status: 400 });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Get site details
|
|
255
|
+
const siteDetails = await getSite(siteId, teamId);
|
|
256
|
+
if (!siteDetails) {
|
|
257
|
+
return new Response("Site not found", { status: 404 });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const events = await request.json() as SiteEventInput[];
|
|
262
|
+
if (!Array.isArray(events)) {
|
|
263
|
+
return new Response("Events must be an array", { status: 400 });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const siteAdapter = (siteDetails.site_db_adapter ?? "sqlite") as DBAdapter;
|
|
267
|
+
const result = await seedEvents(siteId, siteDetails.uuid, siteAdapter, events);
|
|
268
|
+
|
|
269
|
+
if (result.success) {
|
|
270
|
+
return Response.json({
|
|
271
|
+
success: true,
|
|
272
|
+
inserted: result.inserted || 0,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return Response.json({ success: false, error: result.error }, { status: 500 });
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error("Error seeding events:", error);
|
|
279
|
+
return new Response(
|
|
280
|
+
error instanceof Error ? error.message : "Failed to seed events",
|
|
281
|
+
{ status: 500 }
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return new Response("Not Found", { status: 404 });
|
|
287
|
+
}
|
|
288
|
+
);
|