fix: analytics and deprecated warning

This commit is contained in:
Michael Freno
2026-01-21 13:58:34 -05:00
parent 7b60494d6d
commit 955c856a85
9 changed files with 95 additions and 55 deletions

View File

@@ -1,6 +1,5 @@
import type { APIEvent } from "@solidjs/start/server"; import type { APIEvent } from "@solidjs/start/server";
import { appRouter } from "~/server/api/root"; import { createServerCaller } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/utils";
export async function GET(event: APIEvent) { export async function GET(event: APIEvent) {
const url = new URL(event.request.url); const url = new URL(event.request.url);
@@ -31,8 +30,7 @@ export async function GET(event: APIEvent) {
try { try {
console.log("[GitHub OAuth Callback] Creating tRPC caller..."); console.log("[GitHub OAuth Callback] Creating tRPC caller...");
const ctx = await createTRPCContext(event); const caller = await createServerCaller(event);
const caller = appRouter.createCaller(ctx);
console.log("[GitHub OAuth Callback] Calling githubCallback procedure..."); console.log("[GitHub OAuth Callback] Calling githubCallback procedure...");
const result = await caller.auth.githubCallback({ code }); const result = await caller.auth.githubCallback({ code });

View File

@@ -1,6 +1,5 @@
import type { APIEvent } from "@solidjs/start/server"; import type { APIEvent } from "@solidjs/start/server";
import { appRouter } from "~/server/api/root"; import { createServerCaller } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/utils";
export async function GET(event: APIEvent) { export async function GET(event: APIEvent) {
const url = new URL(event.request.url); const url = new URL(event.request.url);
@@ -31,8 +30,7 @@ export async function GET(event: APIEvent) {
try { try {
console.log("[Google OAuth Callback] Creating tRPC caller..."); console.log("[Google OAuth Callback] Creating tRPC caller...");
const ctx = await createTRPCContext(event); const caller = await createServerCaller(event);
const caller = appRouter.createCaller(ctx);
console.log("[Google OAuth Callback] Calling googleCallback procedure..."); console.log("[Google OAuth Callback] Calling googleCallback procedure...");
const result = await caller.auth.googleCallback({ code }); const result = await caller.auth.googleCallback({ code });

View File

@@ -1,6 +1,5 @@
import type { APIEvent } from "@solidjs/start/server"; import type { APIEvent } from "@solidjs/start/server";
import { appRouter } from "~/server/api/root"; import { createServerCaller } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/utils";
export async function GET(event: APIEvent) { export async function GET(event: APIEvent) {
const url = new URL(event.request.url); const url = new URL(event.request.url);
@@ -27,8 +26,7 @@ export async function GET(event: APIEvent) {
try { try {
console.log("[Email Login Callback] Creating tRPC caller..."); console.log("[Email Login Callback] Creating tRPC caller...");
// Create tRPC caller to invoke the emailLogin procedure // Create tRPC caller to invoke the emailLogin procedure
const ctx = await createTRPCContext(event); const caller = await createServerCaller(event);
const caller = appRouter.createCaller(ctx);
console.log("[Email Login Callback] Calling emailLogin procedure..."); console.log("[Email Login Callback] Calling emailLogin procedure...");
// Call the email login handler - rememberMe will be read from JWT payload // Call the email login handler - rememberMe will be read from JWT payload

View File

@@ -1,6 +1,5 @@
import type { APIEvent } from "@solidjs/start/server"; import type { APIEvent } from "@solidjs/start/server";
import { appRouter } from "~/server/api/root"; import { createServerCaller } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/utils";
export async function GET(event: APIEvent) { export async function GET(event: APIEvent) {
const url = new URL(event.request.url); const url = new URL(event.request.url);
@@ -57,20 +56,19 @@ export async function GET(event: APIEvent) {
`, `,
{ {
status: 400, status: 400,
headers: { "Content-Type": "text/html" }, headers: { "Content-Type": "text/html" }
} }
); );
} }
try { try {
// Create tRPC caller to invoke the emailVerification procedure // Create tRPC caller to invoke the emailVerification procedure
const ctx = await createTRPCContext(event); const caller = await createServerCaller(event);
const caller = appRouter.createCaller(ctx);
// Call the email verification handler // Call the email verification handler
const result = await caller.auth.emailVerification({ const result = await caller.auth.emailVerification({
email, email,
token, token
}); });
if (result.success) { if (result.success) {
@@ -129,7 +127,7 @@ export async function GET(event: APIEvent) {
`, `,
{ {
status: 200, status: 200,
headers: { "Content-Type": "text/html" }, headers: { "Content-Type": "text/html" }
} }
); );
} else { } else {
@@ -139,8 +137,10 @@ export async function GET(event: APIEvent) {
console.error("Email verification callback error:", error); console.error("Email verification callback error:", error);
// Check if it's a token expiration error // Check if it's a token expiration error
const errorMessage = error instanceof Error ? error.message : "server_error"; const errorMessage =
const isTokenError = errorMessage.includes("expired") || errorMessage.includes("invalid"); error instanceof Error ? error.message : "server_error";
const isTokenError =
errorMessage.includes("expired") || errorMessage.includes("invalid");
return new Response( return new Response(
` `
@@ -192,7 +192,7 @@ export async function GET(event: APIEvent) {
`, `,
{ {
status: 400, status: 400,
headers: { "Content-Type": "text/html" }, headers: { "Content-Type": "text/html" }
} }
); );
} }

View File

@@ -13,8 +13,9 @@ import { accountRouter } from "./routers/account";
import { downloadsRouter } from "./routers/downloads"; import { downloadsRouter } from "./routers/downloads";
import { cairnDbRouter } from "./routers/cairn"; import { cairnDbRouter } from "./routers/cairn";
import { appleNotificationsRouter } from "./routers/apple-notifications"; import { appleNotificationsRouter } from "./routers/apple-notifications";
import { createTRPCRouter, createTRPCContext } from "./utils"; import { createTRPCRouter, createTRPCContext, t } from "./utils";
import type { H3Event } from "h3"; import type { H3Event } from "h3";
import type { APIEvent } from "@solidjs/start/server";
export const appRouter = createTRPCRouter({ export const appRouter = createTRPCRouter({
auth: authRouter, auth: authRouter,
@@ -36,12 +37,24 @@ export const appRouter = createTRPCRouter({
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;
/** Server-side caller factory using the modern tRPC pattern */
export const createCallerFactory = t.createCallerFactory(appRouter);
/** /**
* Create a server-side caller for tRPC procedures * Create a server-side caller for tRPC procedures from H3Event (vinxi/http getEvent)
* This allows calling tRPC procedures directly on the server with proper context * Used in server functions within route files
*/ */
export const createCaller = async (event: H3Event) => { export const createCaller = async (event: H3Event) => {
const apiEvent = { nativeEvent: event, request: event.node.req } as any; const apiEvent = { nativeEvent: event, request: event.node.req } as any;
const ctx = await createTRPCContext(apiEvent); const ctx = await createTRPCContext(apiEvent);
return appRouter.createCaller(ctx); return createCallerFactory(ctx);
};
/**
* Create a server-side caller for tRPC procedures from APIEvent
* Used in API route handlers
*/
export const createServerCaller = async (event: APIEvent) => {
const ctx = await createTRPCContext(event);
return createCallerFactory(ctx);
}; };

View File

@@ -5,7 +5,6 @@ import {
getAnalyticsSummary, getAnalyticsSummary,
getPathAnalytics, getPathAnalytics,
cleanupOldAnalytics, cleanupOldAnalytics,
logVisit,
getPerformanceStats, getPerformanceStats,
enrichAnalyticsEntry enrichAnalyticsEntry
} from "~/server/analytics"; } from "~/server/analytics";
@@ -13,6 +12,26 @@ import { ConnectionFactory } from "~/server/database";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { getRequestIP } from "vinxi/http"; import { getRequestIP } from "vinxi/http";
/** Safely get a header value from either Fetch API Headers or Node.js IncomingHttpHeaders */
function getHeader(
headers: Record<string, string | string[] | undefined> | Headers | undefined,
name: string
): string | undefined {
if (!headers) return undefined;
// Check if it's a Fetch API Headers object (has .get method)
if (typeof (headers as Headers).get === "function") {
return (headers as Headers).get(name) || undefined;
}
// Otherwise treat as Node.js IncomingHttpHeaders (plain object)
const value = (headers as Record<string, string | string[] | undefined>)[
name.toLowerCase()
];
if (Array.isArray(value)) return value[0];
return value;
}
export const analyticsRouter = createTRPCRouter({ export const analyticsRouter = createTRPCRouter({
logPerformance: publicProcedure logPerformance: publicProcedure
.input( .input(
@@ -71,14 +90,12 @@ export const analyticsRouter = createTRPCRouter({
} else { } else {
const req = ctx.event.nativeEvent.node?.req || ctx.event.nativeEvent; const req = ctx.event.nativeEvent.node?.req || ctx.event.nativeEvent;
const userAgent = const userAgent =
req.headers?.["user-agent"] || getHeader(req.headers, "user-agent") ||
ctx.event.request?.headers?.get("user-agent") || getHeader(ctx.event.request?.headers, "user-agent");
undefined;
const referrer = const referrer =
req.headers?.referer || getHeader(req.headers, "referer") ||
req.headers?.referrer || getHeader(req.headers, "referrer") ||
ctx.event.request?.headers?.get("referer") || getHeader(ctx.event.request?.headers, "referer");
undefined;
const ipAddress = getRequestIP(ctx.event.nativeEvent) || undefined; const ipAddress = getRequestIP(ctx.event.nativeEvent) || undefined;
const enriched = enrichAnalyticsEntry({ const enriched = enrichAnalyticsEntry({
userId: ctx.userId, userId: ctx.userId,

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { appRouter } from "~/server/api/root"; import { createCallerFactory, appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/utils"; import { createTRPCContext } from "~/server/api/utils";
vi.mock("~/server/apple-notification", () => ({ vi.mock("~/server/apple-notification", () => ({
@@ -17,9 +17,10 @@ vi.mock("~/server/apple-notification-store", () => ({
describe("apple notification router", () => { describe("apple notification router", () => {
it("verifies and stores notifications", async () => { it("verifies and stores notifications", async () => {
const caller = appRouter.createCaller( const ctx = await createTRPCContext({
await createTRPCContext({ nativeEvent: { node: { req: {} } } } as any) nativeEvent: { node: { req: {} } }
); } as any);
const caller = createCallerFactory(ctx);
const result = await caller.appleNotifications.verifyAndStore.mutate({ const result = await caller.appleNotifications.verifyAndStore.mutate({
signedPayload: "test" signedPayload: "test"

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { appRouter } from "~/server/api/root"; import { createCallerFactory } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/utils"; import { createTRPCContext } from "~/server/api/utils";
// Mock the S3 client and getSignedUrl function // Mock the S3 client and getSignedUrl function
@@ -33,9 +33,8 @@ process.env.VITE_DOWNLOAD_BUCKET_STRING = "test-bucket";
describe("downloads router", () => { describe("downloads router", () => {
it("should return a signed URL for valid asset names", async () => { it("should return a signed URL for valid asset names", async () => {
const caller = appRouter.createCaller( const ctx = await createTRPCContext({ nativeEvent: {} } as any);
await createTRPCContext({ nativeEvent: {} } as any) const caller = createCallerFactory(ctx);
);
const result = await caller.downloads.getDownloadUrl.query({ const result = await caller.downloads.getDownloadUrl.query({
asset_name: "lineage" asset_name: "lineage"
@@ -46,9 +45,8 @@ describe("downloads router", () => {
}); });
it("should throw NOT_FOUND for invalid asset names", async () => { it("should throw NOT_FOUND for invalid asset names", async () => {
const caller = appRouter.createCaller( const ctx = await createTRPCContext({ nativeEvent: {} } as any);
await createTRPCContext({ nativeEvent: {} } as any) const caller = createCallerFactory(ctx);
);
try { try {
await caller.downloads.getDownloadUrl.query({ await caller.downloads.getDownloadUrl.query({

View File

@@ -12,6 +12,26 @@ export type Context = {
cairnUserId: string | null; cairnUserId: string | null;
}; };
/** Safely get a header value from either Fetch API Headers or Node.js IncomingHttpHeaders */
function getHeader(
headers: Record<string, string | string[] | undefined> | Headers | undefined,
name: string
): string | undefined {
if (!headers) return undefined;
// Check if it's a Fetch API Headers object (has .get method)
if (typeof (headers as Headers).get === "function") {
return (headers as Headers).get(name) || undefined;
}
// Otherwise treat as Node.js IncomingHttpHeaders (plain object)
const value = (headers as Record<string, string | string[] | undefined>)[
name.toLowerCase()
];
if (Array.isArray(value)) return value[0];
return value;
}
async function createContextInner(event: APIEvent): Promise<Context> { async function createContextInner(event: APIEvent): Promise<Context> {
const payload = await getAuthPayloadFromEvent(event.nativeEvent); const payload = await getAuthPayloadFromEvent(event.nativeEvent);
@@ -27,19 +47,16 @@ async function createContextInner(event: APIEvent): Promise<Context> {
const path = req.url || event.request?.url || "unknown"; const path = req.url || event.request?.url || "unknown";
const method = req.method || event.request?.method || "GET"; const method = req.method || event.request?.method || "GET";
const userAgent = const userAgent =
req.headers?.["user-agent"] || getHeader(req.headers, "user-agent") ||
event.request?.headers?.get("user-agent") || getHeader(event.request?.headers, "user-agent");
undefined;
const referrer = const referrer =
req.headers?.referer || getHeader(req.headers, "referer") ||
req.headers?.referrer || getHeader(req.headers, "referrer") ||
event.request?.headers?.get("referer") || getHeader(event.request?.headers, "referer");
undefined;
const ipAddress = getRequestIP(event.nativeEvent) || undefined; const ipAddress = getRequestIP(event.nativeEvent) || undefined;
const authHeader = const authHeader =
event.request?.headers?.get("authorization") || getHeader(req.headers, "authorization") ||
req.headers?.authorization || getHeader(event.request?.headers, "authorization") ||
req.headers?.Authorization ||
null; null;
let cairnUserId: string | null = null; let cairnUserId: string | null = null;