Skip to content

Commit

Permalink
Raw project query (#382)
Browse files Browse the repository at this point in the history
  • Loading branch information
N2D4 authored Dec 28, 2024
1 parent 2aa6c31 commit cf95bb7
Show file tree
Hide file tree
Showing 17 changed files with 545 additions and 218 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");
3 changes: 2 additions & 1 deletion apps/backend/scripts/verify-data-integrity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PrismaClient } from "@prisma/client";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
import { wait } from "@stackframe/stack-shared/dist/utils/promises";
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
Expand Down Expand Up @@ -88,6 +88,7 @@ async function main() {
},
}),
]);
if (users.pagination?.next_cursor) throwErr("Users are paginated? Please update the verify-data-integrity.ts script to handle this.");

for (let j = 0; j < users.items.length; j++) {
const user = users.items[j];
Expand Down
4 changes: 2 additions & 2 deletions 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 Expand Up @@ -339,7 +339,7 @@ export function getUserQuery(projectId: string, userId: string): RawQuery<UsersC
`,
postProcess: (queryResult) => {
if (queryResult.length !== 1) {
throw new StackAssertionError("Expected 1 result, got " + queryResult.length, queryResult);
throw new StackAssertionError(`Expected 1 user with id ${userId} in project ${projectId}, got ${queryResult.length}`, { queryResult });
}

const row = queryResult[0].row_data_json;
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/lib/openapi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { yupNumber, yupObject, yupString } from '@stackframe/stack-shared/dist/s
import { StackAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors';
import { HttpMethod } from '@stackframe/stack-shared/dist/utils/http';
import { typedEntries, typedFromEntries } from '@stackframe/stack-shared/dist/utils/objects';
import { deindent } from '@stackframe/stack-shared/dist/utils/strings';
import { deindent, stringCompare } from '@stackframe/stack-shared/dist/utils/strings';
import * as yup from 'yup';

export function parseOpenAPI(options: {
Expand Down Expand Up @@ -34,7 +34,7 @@ export function parseOpenAPI(options: {
)]
))
.filter(([_, handlersByMethod]) => Object.keys(handlersByMethod).length > 0)
.sort(([_a, handlersByMethodA], [_b, handlersByMethodB]) => ((Object.values(handlersByMethodA)[0] as any).tags[0] ?? "").localeCompare(((Object.values(handlersByMethodB)[0] as any).tags[0] ?? ""))),
.sort(([_a, handlersByMethodA], [_b, handlersByMethodB]) => stringCompare((Object.values(handlersByMethodA)[0] as any).tags[0] ?? "", (Object.values(handlersByMethodB)[0] as any).tags[0] ?? "")),
),
};
}
Expand Down
59 changes: 37 additions & 22 deletions apps/backend/src/lib/permissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { KnownErrors } from "@stackframe/stack-shared";
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
import { TeamPermissionDefinitionsCrud, TeamPermissionsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-permissions";
import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { typedToLowercase, typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
import { stringCompare, typedToLowercase, typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
import { PrismaTransaction } from "./types";

export const fullPermissionInclude = {
Expand Down Expand Up @@ -36,30 +36,51 @@ const descriptionMap: Record<DBTeamSystemPermission, string> = {
"INVITE_MEMBERS": "Invite other users to the team",
};

export function teamPermissionDefinitionJsonFromDbType(db: Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>): TeamPermissionDefinitionsCrud["Admin"]["Read"] & { __database_id: string }{
type ExtendedTeamPermissionDefinition = TeamPermissionDefinitionsCrud["Admin"]["Read"] & {
__database_id: string,
__is_default_team_member_permission?: boolean,
__is_default_team_creator_permission?: boolean,
};

export function teamPermissionDefinitionJsonFromDbType(db: Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>): ExtendedTeamPermissionDefinition {
return teamPermissionDefinitionJsonFromRawDbType(db);
}

/**
* Can either take a Prisma permission object or a raw SQL `to_jsonb` result.
*/
export function teamPermissionDefinitionJsonFromRawDbType(db: any | Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>): ExtendedTeamPermissionDefinition {
if (!db.projectConfigId && !db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId`, { db });
if (db.projectConfigId && db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId, not both`, { db });
if (db.scope === "GLOBAL" && db.teamId) throw new StackAssertionError(`Permission DB object should not have teamId when scope is GLOBAL`, { db });

return {
__database_id: db.dbId,
__is_default_team_member_permission: db.isDefaultTeamMemberPermission,
__is_default_team_creator_permission: db.isDefaultTeamCreatorPermission,
id: db.queryableId,
description: db.description || undefined,
contained_permission_ids: db.parentEdges.map((edge) => {
contained_permission_ids: db.parentEdges?.map((edge: any) => {
if (edge.parentPermission) {
return edge.parentPermission.queryableId;
} else if (edge.parentTeamSystemPermission) {
return '$' + typedToLowercase(edge.parentTeamSystemPermission);
} else {
throw new StackAssertionError(`Permission edge should have either parentPermission or parentSystemPermission`, { edge });
}
}).sort(),
}).sort() ?? [],
} as const;
}

export function teamPermissionDefinitionJsonFromTeamSystemDbType(db: DBTeamSystemPermission): TeamPermissionDefinitionsCrud["Admin"]["Read"] & { __database_id: string } {
export function teamPermissionDefinitionJsonFromTeamSystemDbType(db: DBTeamSystemPermission, projectConfig: { teamCreateDefaultSystemPermissions: string[] | null, teamMemberDefaultSystemPermissions: string[] | null }): ExtendedTeamPermissionDefinition {
if ((["teamMemberDefaultSystemPermissions", "teamCreateDefaultSystemPermissions"] as const).some(key => projectConfig[key] !== null && !Array.isArray(projectConfig[key]))) {
throw new StackAssertionError(`Project config should have (nullable) array values for teamMemberDefaultSystemPermissions and teamCreateDefaultSystemPermissions`, { projectConfig });
}

return {
__database_id: '$' + typedToLowercase(db),
__is_default_team_member_permission: projectConfig.teamMemberDefaultSystemPermissions?.includes(db) ?? false,
__is_default_team_creator_permission: projectConfig.teamCreateDefaultSystemPermissions?.includes(db) ?? false,
id: '$' + typedToLowercase(db),
description: descriptionMap[db],
contained_permission_ids: [] as string[],
Expand Down Expand Up @@ -146,11 +167,7 @@ export async function listUserTeamPermissions(
}

return finalResults
.sort((a, b) => {
if (a.team_id !== b.team_id) return a.team_id.localeCompare(b.team_id);
if (a.user_id !== b.user_id) return a.user_id.localeCompare(b.user_id);
return a.id.localeCompare(b.id);
})
.sort((a, b) => stringCompare(a.team_id, b.team_id) || stringCompare(a.user_id, b.user_id) || stringCompare(a.id, b.id))
.filter(p => options.permissionId ? p.id === options.permissionId : true);
}

Expand Down Expand Up @@ -307,25 +324,23 @@ export async function listTeamPermissionDefinitions(
tx: PrismaTransaction,
project: ProjectsCrud["Admin"]["Read"]
): Promise<(TeamPermissionDefinitionsCrud["Admin"]["Read"] & { __database_id: string })[]> {
const res = await tx.permission.findMany({
const projectConfig = await tx.projectConfig.findUnique({
where: {
projectConfig: {
projects: {
some: {
id: project.id,
}
}
id: project.config.id,
},
include: {
permissions: {
include: fullPermissionInclude,
},
scope: "TEAM",
},
orderBy: { queryableId: 'asc' },
include: fullPermissionInclude,
});
if (!projectConfig) throw new StackAssertionError(`Couldn't find project config`, { project });
const res = projectConfig.permissions;
const nonSystemPermissions = res.map(db => teamPermissionDefinitionJsonFromDbType(db));

const systemPermissions = Object.values(DBTeamSystemPermission).map(db => teamPermissionDefinitionJsonFromTeamSystemDbType(db));
const systemPermissions = Object.values(DBTeamSystemPermission).map(db => teamPermissionDefinitionJsonFromTeamSystemDbType(db, projectConfig));

return [...nonSystemPermissions, ...systemPermissions];
return [...nonSystemPermissions, ...systemPermissions].sort((a, b) => stringCompare(a.id, b.id));
}

export async function createTeamPermissionDefinition(
Expand Down
Loading

0 comments on commit cf95bb7

Please sign in to comment.