Skip to content

Commit

Permalink
Faster
Browse files Browse the repository at this point in the history
  • Loading branch information
N2D4 committed Dec 28, 2024
1 parent 0d3870a commit 834b93d
Show file tree
Hide file tree
Showing 4 changed files with 14 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- It's very common to query by userId, projectId, and eventStartedAt at the same time.
-- We can use a composite index to speed up the query.
-- Sadly we can't add this to the Prisma schema itself because Prisma does not understand composite indexes of JSONB fields.
-- So we have to add it manually.
CREATE INDEX idx_event_userid_projectid_eventstartedat ON "Event" ((data->>'projectId'), (data->>'userId'), "eventStartedAt");
2 changes: 1 addition & 1 deletion apps/backend/src/app/api/v1/users/crud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export function getUserQuery(projectId: string, userId: string): RawQuery<UsersC
'lastActiveAt', (
SELECT MAX("eventStartedAt") as "lastActiveAt"
FROM "Event"
WHERE data->>'projectId' = "ProjectUser"."projectId" AND ("data"->>'userId')::UUID = "ProjectUser"."projectUserId" AND "systemEventTypeIds" @> '{"$user-activity"}'
WHERE data->>'projectId' = "ProjectUser"."projectId" AND "data"->>'userId' = ("ProjectUser"."projectUserId")::text AND "systemEventTypeIds" @> '{"$user-activity"}'
),
'ContactChannels', (
SELECT COALESCE(ARRAY_AGG(
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/prisma-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export async function rawQueryAll<Q extends Record<string, undefined | RawQuery<

async function rawQueryArray<Q extends RawQuery<any>[]>(queries: Q): Promise<[] & { [K in keyof Q]: Awaited<ReturnType<Q[K]["postProcess"]>> }> {
return await traceSpan({
description: "raw SQL query",
description: `raw SQL quer${queries.length === 1 ? "y" : `ies (${queries.length} total)`}`,
attributes: {
"stack.raw-queries.length": queries.length,
...Object.fromEntries(queries.map((q, index) => [`stack.raw-queries.${index}`, q.sql.text])),
Expand Down
28 changes: 7 additions & 21 deletions apps/backend/src/route-handlers/smart-request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ import "../polyfills";

import { getUser, getUserQuery } from "@/app/api/v1/users/crud";
import { checkApiKeySet, checkApiKeySetQuery } from "@/lib/api-keys";
import { getProject, listManagedProjectIds } from "@/lib/projects";
import { getProjectQuery, listManagedProjectIds } from "@/lib/projects";
import { decodeAccessToken } from "@/lib/tokens";
import { rawQueryAll } from "@/prisma-client";
import { traceSpan, withTraceSpan } from "@/utils/telemetry";
import { withTraceSpan } from "@/utils/telemetry";
import { KnownErrors } from "@stackframe/stack-shared";
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
import { StackAdaptSentinel, yupValidate } from "@stackframe/stack-shared/dist/schema-fields";
import { groupBy, typedIncludes } from "@stackframe/stack-shared/dist/utils/arrays";
import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { ignoreUnhandledRejection } from "@stackframe/stack-shared/dist/utils/promises";
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
import { NextRequest } from "next/server";
import * as yup from "yup";
Expand Down Expand Up @@ -200,23 +199,10 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque
isClientKeyValid: projectId && publishableClientKey && requestType === "client" ? checkApiKeySetQuery(projectId, { publishableClientKey }) : undefined,
isServerKeyValid: projectId && secretServerKey && requestType === "server" ? checkApiKeySetQuery(projectId, { secretServerKey }) : undefined,
isAdminKeyValid: projectId && superSecretAdminKey && requestType === "admin" ? checkApiKeySetQuery(projectId, { superSecretAdminKey }) : undefined,
project: projectId ? getProjectQuery(projectId) : undefined,
};
const queriesResults = await rawQueryAll(bundledQueries);

const queryFuncs = {
project: () => projectId ? getProject(projectId) : Promise.resolve(null),
} as const;
const results: [string, Promise<any>][] = [];
for (const [key, func] of Object.entries(queryFuncs)) {
results.push([
key,
ignoreUnhandledRejection(traceSpan(`auth query ${key}`, async () => {
return await func();
})),
]);
}
const queries = Object.fromEntries(results) as { [K in keyof typeof queryFuncs]: ReturnType<typeof queryFuncs[K]> };

const eitherKeyOrToken = !!(publishableClientKey || secretServerKey || superSecretAdminKey || adminAccessToken);

if (!requestType && eitherKeyOrToken) {
Expand All @@ -233,9 +219,9 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque
const result = await checkApiKeySet("internal", { superSecretAdminKey: developmentKeyOverride });
if (!result) throw new StatusError(401, "Invalid development key override");
} else if (adminAccessToken) {
// TODO put this into the bundled queries above (not so important because this path is quite rare)
await extractUserFromAdminAccessToken({ token: adminAccessToken, projectId });
if (!await queries.project) {
// TODO put the assertion below into the bundled queries above (not so important because this path is quite rare)
await extractUserFromAdminAccessToken({ token: adminAccessToken, projectId }); // assert that the admin token is valid
if (!queriesResults.project) {
// this happens if the project is still in the user's managedProjectIds, but has since been deleted
throw new KnownErrors.InvalidProjectForAdminAccessToken();
}
Expand All @@ -262,7 +248,7 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque
}
}

const project = await queries.project;
const project = queriesResults.project;
if (!project) {
throw new StackAssertionError("Project not found; this should never happen because passing the checks until here should guarantee that the project exists and that access to it is granted", { projectId });
}
Expand Down

0 comments on commit 834b93d

Please sign in to comment.