diff --git a/src/pages/api/v0/check_email.ts b/src/pages/api/v0/check_email.ts index 163b5efc..e3183dbd 100644 --- a/src/pages/api/v0/check_email.ts +++ b/src/pages/api/v0/check_email.ts @@ -32,7 +32,7 @@ const POST = async ( if (sentResponse) { return; } - let d1 = performance.now() - startTime; + const d1 = performance.now() - startTime; console.log(`[🐢] checkUserInDB: ${Math.round(d1)}ms`); try { @@ -66,7 +66,7 @@ const POST = async ( async function (msg) { if (msg?.properties.correlationId === verificationId) { const output = JSON.parse(msg.content.toString()); - let d5 = performance.now() - startTime; + const d5 = performance.now() - startTime; console.log( `[🐢] Got consume message: ${Math.round(d5)}ms` ); @@ -94,21 +94,31 @@ const POST = async ( res.status(response.status).json(response.error); return; } - let d6 = performance.now() - startTime; + const d6 = performance.now() - startTime; console.log(`[🐢] Add to supabase: ${Math.round(d6)}ms`); - await ch1.close(); - let d7 = performance.now() - startTime; - console.log(`[🐢] ch1.close: ${Math.round(d7)}ms`); - await conn.close(); - let d8 = performance.now() - startTime; - console.log(`[🐢] conn.close: ${Math.round(d8)}ms`); - - await updateSendinblue(user); - let d9 = performance.now() - startTime; - console.log(`[🐢] updateSendinblue: ${Math.round(d9)}ms`); + // Cleanup + await Promise.all([ + updateSendinblue(user).then(() => { + const d8 = performance.now() - startTime; + console.log( + `[🐢] updateSendinblue: ${Math.round(d8)}ms` + ); + }), + ch1 + .close() + .then(() => conn.close()) + .then(() => { + const d7 = performance.now() - startTime; + console.log( + `[🐢] ch1.close: ${Math.round(d7)}ms` + ); + }), + ]); res.status(200).json(output); + const d9 = performance.now() - startTime; + console.log(`[🐢] Final response: ${Math.round(d9)}ms`); } }, { @@ -116,7 +126,7 @@ const POST = async ( } ); - let d2 = performance.now() - startTime; + const d2 = performance.now() - startTime; console.log(`[🐢] AMQP setup: ${Math.round(d2)}ms`); const verifMethod = await getVerifMethod(req.body as CheckEmailInput); @@ -130,7 +140,7 @@ const POST = async ( // reputation. verifMethod === "Api" ? "Headless" : verifMethod }`; - let d3 = performance.now() - startTime; + const d3 = performance.now() - startTime; console.log(`[🐢] getVerifMethod: ${Math.round(d3)}ms`); await ch1.assertQueue(queueName, { @@ -151,7 +161,7 @@ const POST = async ( replyTo: replyQ.queue, } ); - let d4 = performance.now() - startTime; + const d4 = performance.now() - startTime; console.log(`[🐢] sendToQueue: ${Math.round(d4)}ms`); setTimeout(() => { diff --git a/src/util/api.ts b/src/util/api.ts index e0d2c954..e7c8c4c1 100644 --- a/src/util/api.ts +++ b/src/util/api.ts @@ -5,12 +5,8 @@ import { NextApiRequest, NextApiResponse } from "next"; import { RateLimiterRes } from "rate-limiter-flexible"; import { subApiMaxCalls } from "./subs"; -import { SupabaseUser } from "./supabaseClient"; -import { - getActiveSubscription, - getApiUsageServer, - getUserByApiToken, -} from "./supabaseServer"; +import { SupabaseSubscription, SupabaseUser } from "./supabaseClient"; +import { getSubAndCalls, getUserByApiToken } from "./supabaseServer"; // Helper method to wait for a middleware to execute before continuing // And to throw an error when an error happens in a middleware @@ -78,30 +74,35 @@ export async function checkUserInDB( return { sentResponse: true }; } - // Safe to type cast here, as we only need the `id` field below. - const authUser = { id: user.id } as User; - - // TODO instead of doing another round of network call, we should do a - // join for subscriptions and API calls inside getUserByApiToken. - const sub = await getActiveSubscription(authUser); - const used = await getApiUsageServer(user, sub); + const subAndCalls = await getSubAndCalls(user.id); // Set rate limit headers. const now = new Date(); - const nextReset = sub - ? typeof sub.current_period_end === "string" - ? parseISO(sub.current_period_end) - : sub.current_period_end + const nextReset = subAndCalls.subscription_id + ? typeof subAndCalls.current_period_end === "string" + ? parseISO(subAndCalls.current_period_end) + : subAndCalls.current_period_end : addMonths(now, 1); const msDiff = differenceInMilliseconds(nextReset, now); - const max = subApiMaxCalls(sub); + const max = subApiMaxCalls({ + prices: { + products: { + id: subAndCalls.product_id, + }, + }, + } as SupabaseSubscription); setRateLimitHeaders( res, - new RateLimiterRes(max - used - 1, msDiff, used, undefined), // 1st arg has -1, because we just consumed 1 email. + new RateLimiterRes( + max - subAndCalls.number_of_calls - 1, + msDiff, + subAndCalls.number_of_calls, + undefined + ), // 1st arg has -1, because we just consumed 1 email. max ); - if (used > max) { + if (subAndCalls.number_of_calls > max) { res.status(429).json({ error: "Too many requests this month. Please upgrade your Reacher plan to make more requests.", }); diff --git a/src/util/supabaseClient.ts b/src/util/supabaseClient.ts index 68cd9285..7ecec87f 100644 --- a/src/util/supabaseClient.ts +++ b/src/util/supabaseClient.ts @@ -119,8 +119,7 @@ export function updateUserName( .eq("id", user.id); } -// Get the api calls of a user in the past month. Same as -// `getApiUsageServer`, but for client usage. +// Get the api calls of a user in the past month. export async function getApiUsageClient( user: User, subscription: SupabaseSubscription | null | undefined diff --git a/src/util/supabaseServer.ts b/src/util/supabaseServer.ts index 547ce06b..7658f570 100644 --- a/src/util/supabaseServer.ts +++ b/src/util/supabaseServer.ts @@ -52,27 +52,26 @@ export async function getActiveSubscription( return data?.[0]; } -// Get the api calls of a user in the past month. Same as -// `getApiUsageClient`, but for server usage. -export async function getApiUsageServer( - user: SupabaseUser, - subscription: SupabaseSubscription | null | undefined -): Promise { - const { count, error } = await supabaseAdmin - .from("calls") - .select("*", { count: "exact" }) - .eq("user_id", user.id) - .gt("created_at", getUsageStartDate(subscription).toISOString()); +interface SubAndCalls { + user_id: string; + subscription_id: string | null; + product_id: string | null; + email: string; + current_period_start: string | Date; + current_period_end: string | Date; + number_of_calls: number; +} +export async function getSubAndCalls(userId: string): Promise { + const { data, error } = await supabaseAdmin + .from("sub_and_calls") + .select("*") + .eq("user_id", userId) + .single(); if (error) { + console.log("getSubAndCalls error", error); throw error; } - if (count === null) { - throw new Error( - `Got null count in getApiUsageServer for user ${user.id}.` - ); - } - - return count; + return data; } diff --git a/supabase/migrations/20231212094633_performance.sql b/supabase/migrations/20231212094633_performance.sql new file mode 100644 index 00000000..4c34618b --- /dev/null +++ b/supabase/migrations/20231212094633_performance.sql @@ -0,0 +1,49 @@ +/* + This SQL code creates a view called "subs_and_calls" that combines information from three tables: "auth.users", "subscriptions", and "calls". + The view includes the following columns: + - u.id: The user ID from the "auth.users" table. + - u.email: The user's email address from the "auth.users" table. + - s.id AS subscription_id: The subscription ID from the "subscriptions" table. + - s.current_period_start: The start date of the current subscription period from the "subscriptions" table. + - s.current_period_end: The end date of the current subscription period from the "subscriptions" table. + - COUNT(c.id) AS number_of_calls: The number of calls made by the user, calculated by counting the records in the "calls" table. + + The view is created using LEFT JOINs to ensure that all users are included, even if they don't have an active subscription or any calls. + The JOIN conditions filter the subscriptions based on their status being 'active' and the calls based on their creation date falling within the current subscription period or within the last month. + + The result is grouped by the user ID, subscription ID, and current subscription period end date. + + This view can be used to retrieve information about users, their subscriptions, and the number of calls they have made. +*/ +DROP VIEW sub_and_calls; +CREATE VIEW sub_and_calls +WITH (security_invoker = TRUE) +AS + SELECT + u.id as user_id, + s.id AS subscription_id, + s.current_period_start, + s.current_period_end, + pro.id as product_id, + COUNT(c.id) AS number_of_calls + FROM + users u + LEFT JOIN + subscriptions s ON u.id = s.user_id + AND s.status = 'active' + LEFT JOIN + prices pri ON pri.id = s.price_id + LEFT JOIN + products pro on pro.id = pri.product_id + LEFT JOIN + calls c ON u.id = c.user_id + AND ( + (s.current_period_start IS NOT NULL AND s.current_period_end IS NOT NULL AND c.created_at BETWEEN s.current_period_start AND s.current_period_end) + OR + (c.created_at >= NOW() - INTERVAL '1 MONTH') + ) + GROUP BY + u.id, s.id, s.current_period_end, pro.id; + +-- Create in index on api_token to speed up the query. +CREATE INDEX api_token_index ON public.users (api_token); \ No newline at end of file