Skip to content

Commit

Permalink
Raw project query
Browse files Browse the repository at this point in the history
  • Loading branch information
N2D4 committed Dec 27, 2024
1 parent 4d5df15 commit 4bedcca
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 7 deletions.
207 changes: 206 additions & 1 deletion apps/backend/src/lib/projects.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { prismaClient, retryTransaction } from "@/prisma-client";
import { RawQuery, prismaClient, rawQuery, retryTransaction } from "@/prisma-client";
import { Prisma } from "@prisma/client";
import { InternalProjectsCrud, ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { deepPlainEquals } from "@stackframe/stack-shared/dist/utils/objects";
import { typedToLowercase, typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
import { fullPermissionInclude, teamPermissionDefinitionJsonFromDbType, teamPermissionDefinitionJsonFromTeamSystemDbType } from "./permissions";
Expand Down Expand Up @@ -193,7 +195,210 @@ export function listManagedProjectIds(projectUser: UsersCrud["Admin"]["Read"]) {
return managedProjectIds;
}

/**
*
* @param projectId export const fullProjectInclude = {
config: {
include: {
oauthProviderConfigs: {
include: {
proxiedOAuthConfig: true,
standardOAuthConfig: true,
},
},
emailServiceConfig: {
include: {
proxiedEmailServiceConfig: true,
standardEmailServiceConfig: true,
},
},
permissions: {
include: fullPermissionInclude,
},
authMethodConfigs: {
include: {
oauthProviderConfig: {
include: {
proxiedOAuthConfig: true,
standardOAuthConfig: true,
},
},
otpConfig: true,
passwordConfig: true,
passkeyConfig: true,
}
},
connectedAccountConfigs: {
include: {
oauthProviderConfig: {
include: {
proxiedOAuthConfig: true,
standardOAuthConfig: true,
},
},
}
},
domains: true,
},
},
configOverride: true,
_count: {
select: {
users: true, // Count the users related to the project
},
},
} as const satisfies Prisma.ProjectInclude;
* @returns
*/

export function getProjectQuery(projectId: string): RawQuery<ProjectsCrud["Admin"]["Read"] | null> {
const OAuthProviderConfigSql = Prisma.sql`
(
SELECT (
to_jsonb("OAuthProviderConfig") ||
jsonb_build_object(
'ProxiedOAuthConfig', (
SELECT (
to_jsonb("ProxiedOAuthProviderConfig") ||
jsonb_build_object()
)
FROM "ProxiedOAuthProviderConfig"
WHERE "ProxiedOAuthProviderConfig"."id" = "OAuthProviderConfig"."id"
),
'StandardOAuthConfig', (
SELECT (
to_jsonb("StandardOAuthProviderConfig") ||
jsonb_build_object()
)
FROM "StandardOAuthProviderConfig"
WHERE "StandardOAuthProviderConfig"."id" = "OAuthProviderConfig"."id"
)
)
)
FROM "OAuthProviderConfig"
WHERE "OAuthProviderConfig"."id" = "AuthMethodConfig"."oauthProviderConfigId"
)
`;

return {
sql: Prisma.sql`
SELECT to_json(
(
SELECT (
to_jsonb("Project".*) ||
jsonb_build_object(
'OAuthProviderConfigs', ${OAuthProviderConfigSql},
'EmailServiceConfig', (
SELECT (
to_jsonb("EmailServiceConfig") ||
jsonb_build_object(
'ProxiedEmailServiceConfig', (
SELECT (
to_jsonb("ProxiedEmailServiceConfig") ||
jsonb_build_object()
)
FROM "ProxiedEmailServiceConfig"
WHERE "ProxiedEmailServiceConfig"."id" = "EmailServiceConfig"."proxiedEmailServiceConfigId"
),
'StandardEmailServiceConfig', (
SELECT (
to_jsonb("StandardEmailServiceConfig") ||
jsonb_build_object()
)
FROM "StandardEmailServiceConfig"
WHERE "StandardEmailServiceConfig"."id" = "EmailServiceConfig"."standardEmailServiceConfigId"
)
)
)
FROM "EmailServiceConfig"
WHERE "EmailServiceConfig"."id" = "Project"."emailServiceConfigId"
),
'AuthMethodConfigs', (
SELECT COALESCE(ARRAY_AGG(
to_jsonb("AuthMethodConfig") ||
jsonb_build_object(
'OAuthProviderConfig', ${OAuthProviderConfigSql},
'OtpConfig', (
SELECT (
to_jsonb("OtpConfig") ||
jsonb_build_object()
)
FROM "OtpConfig"
WHERE "OtpConfig"."id" = "AuthMethodConfig"."otpConfigId"
),
'PasswordConfig', (
SELECT (
to_jsonb("PasswordConfig") ||
jsonb_build_object()
)
FROM "PasswordConfig"
WHERE "PasswordConfig"."id" = "AuthMethodConfig"."passwordConfigId"
),
'PasskeyConfig', (
SELECT (
to_jsonb("PasskeyConfig") ||
jsonb_build_object()
)
FROM "PasskeyConfig"
WHERE "PasskeyConfig"."id" = "AuthMethodConfig"."passkeyConfigId"
)
)
), '{}')
FROM "AuthMethodConfig"
WHERE "AuthMethodConfig"."projectId" = "Project"."id"
),
'ConnectedAccountConfigs', (
SELECT COALESCE(ARRAY_AGG(
to_jsonb("ConnectedAccountConfig") ||
jsonb_build_object(
'OAuthProviderConfig', ${OAuthProviderConfigSql}
)
), '{}')
FROM "ConnectedAccountConfig"
WHERE "ConnectedAccountConfig"."projectId" = "Project"."id"
),
'Domains', (
SELECT COALESCE(ARRAY_AGG(
to_jsonb("ProjectDomain") ||
jsonb_build_object()
), '{}')
FROM "ProjectDomain"
WHERE "ProjectDomain"."projectId" = "Project"."id"
)
)
)
FROM "Project"
LEFT JOIN "ProjectConfig" ON "ProjectConfig"."id" = "Project"."configId"
WHERE "Project"."id" = ${projectId}
)
) AS "row_data_json"
`,
postProcess: (result) => {
throw new Error();
},
} as const;
}

export async function getProject(projectId: string): Promise<ProjectsCrud["Admin"]["Read"] | null> {
const result = await rawQuery(getProjectQuery(projectId));

// In non-prod environments, let's also call the legacy function and ensure the result is the same
// TODO next-release: remove this
if (!getNodeEnvironment().includes("prod")) {
const legacyResult = await getProjectLegacy(projectId);
if (!deepPlainEquals(result, legacyResult)) {
throw new StackAssertionError("Project result mismatch", {
result,
legacyResult,
});
}
}

return result.project;
}

async function getProjectLegacy(projectId: string): Promise<ProjectsCrud["Admin"]["Read"] | null> {
const rawProject = await prismaClient.project.findUnique({
where: { id: projectId },
include: fullProjectInclude,
Expand Down
6 changes: 3 additions & 3 deletions packages/stack-shared/src/interface/crud/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,17 @@ export const projectsCrud = createCrud({
docs: {
clientRead: {
summary: 'Get the current project',
description: 'Get the current project information including display name, oauth providers and authentication methods. Useful for display the available login options to the user.',
description: 'Get the current project information including display name, OAuth providers and authentication methods. Useful for display the available login options to the user.',
tags: ['Projects'],
},
adminRead: {
summary: 'Get the current project',
description: 'Get the current project information and configuration including display name, oauth providers, email configuration, etc.',
description: 'Get the current project information and configuration including display name, OAuth providers, email configuration, etc.',
tags: ['Projects'],
},
adminUpdate: {
summary: 'Update the current project',
description: 'Update the current project information and configuration including display name, oauth providers, email configuration, etc.',
description: 'Update the current project information and configuration including display name, OAuth providers, email configuration, etc.',
tags: ['Projects'],
},
adminDelete: {
Expand Down
7 changes: 4 additions & 3 deletions packages/stack-shared/src/utils/errors.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { globalVar } from "./globals";
import { Json } from "./json";
import { pick } from "./objects";
import { nicify } from "./strings";


export function throwErr(errorMessage: string, extraData?: any): never;
Expand Down Expand Up @@ -81,10 +82,10 @@ StackAssertionError.prototype.name = "StackAssertionError";

export function errorToNiceString(error: unknown): string {
if (!(error instanceof Error)) return `${typeof error}<${error}>`;
const stack = error.stack ?? "";
let stack = error.stack ?? "";
const toString = error.toString();
if (stack.startsWith(toString)) return stack;
return `${toString}\n${stack}`;
if (!stack.startsWith(toString)) stack = `${toString}\n${stack}`; // some browsers don't include the error message in the stack, some do
return `${stack} ${nicify(Object.fromEntries(Object.entries(error)))}`;
}


Expand Down

0 comments on commit 4bedcca

Please sign in to comment.