mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Route read-only raw Prisma queries to read replica (#1467)
This commit is contained in:
parent
0c6e135c30
commit
197ad09eea
@ -119,3 +119,4 @@ To see all development ports, refer to the index.html of `apps/dev-launchpad/pub
|
||||
|
||||
### Code-related
|
||||
- Use ES6 maps instead of records wherever you can.
|
||||
- **Read replicas for raw Prisma queries**: When writing raw SQL queries (`$queryRaw`, `$queryRawUnsafe`), always use `$replica()` for read-only queries (e.g. `globalPrismaClient.$replica().$queryRaw\`SELECT ...\``). This routes reads to the database replica and reduces load on the primary. Do NOT use `$replica()` for queries inside transactions or queries containing writes (INSERT/UPDATE/DELETE, even in CTEs).
|
||||
|
||||
@ -103,7 +103,7 @@ async function getPendingCliAuthAttempt(tenancy: Tenancy, loginCode: string) {
|
||||
// CliAuthAttempt lives in the tenancy's source-of-truth DB, consistent with cli/poll/route.tsx.
|
||||
const prisma = await getPrismaClientForTenancy(tenancy);
|
||||
const schema = await getPrismaSchemaForTenancy(tenancy);
|
||||
const rows = await prisma.$queryRaw<CliAuthAttemptRow[]>(Prisma.sql`
|
||||
const rows = await prisma.$replica().$queryRaw<CliAuthAttemptRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
"id",
|
||||
"tenancyId",
|
||||
@ -130,7 +130,7 @@ async function getPendingCliAuthAttempt(tenancy: Tenancy, loginCode: string) {
|
||||
|
||||
async function getRefreshTokenSession(tenancyId: string, refreshToken: string) {
|
||||
// ProjectUserRefreshToken lives in the global DB (see tokens.tsx and oauth/model.tsx).
|
||||
const rows = await globalPrismaClient.$queryRaw<RefreshTokenRow[]>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<RefreshTokenRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
"id",
|
||||
"tenancyId",
|
||||
|
||||
@ -48,7 +48,7 @@ export const POST = createSmartRouteHandler({
|
||||
const prisma = await getPrismaClientForTenancy(tenancy);
|
||||
const schema = await getPrismaSchemaForTenancy(tenancy);
|
||||
|
||||
const cliAuthRows = await prisma.$queryRaw<CliAuthAttemptRow[]>(Prisma.sql`
|
||||
const cliAuthRows = await prisma.$replica().$queryRaw<CliAuthAttemptRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
"id",
|
||||
"refreshToken",
|
||||
|
||||
@ -42,7 +42,7 @@ export const POST = createSmartRouteHandler({
|
||||
let anonRefreshToken: string | null = null;
|
||||
|
||||
if (anon_refresh_token != null) {
|
||||
const refreshTokenRows = await globalPrismaClient.$queryRaw<RefreshTokenRow[]>(Prisma.sql`
|
||||
const refreshTokenRows = await globalPrismaClient.$replica().$queryRaw<RefreshTokenRow[]>(Prisma.sql`
|
||||
SELECT "tenancyId", "projectUserId", "expiresAt"
|
||||
FROM "ProjectUserRefreshToken"
|
||||
WHERE "refreshToken" = ${anon_refresh_token}
|
||||
|
||||
@ -310,7 +310,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
? Prisma.sql`WHERE "tenancyId" = ${tenancyId}::uuid`
|
||||
: Prisma.sql``;
|
||||
|
||||
const projectUserStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const projectUserStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -321,7 +321,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Project user stats query returned no rows.");
|
||||
|
||||
const contactChannelStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const contactChannelStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -332,7 +332,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Contact channel stats query returned no rows.");
|
||||
|
||||
const teamStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const teamStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -343,7 +343,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Team stats query returned no rows.");
|
||||
|
||||
const teamMemberStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const teamMemberStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -354,7 +354,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Team member stats query returned no rows.");
|
||||
|
||||
const teamPermissionStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const teamPermissionStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -365,7 +365,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Team permission stats query returned no rows.");
|
||||
|
||||
const teamInvitationStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const teamInvitationStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -378,7 +378,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
: Prisma.sql`WHERE "type" = 'TEAM_INVITATION'`}
|
||||
`).at(0) ?? throwErr("Team invitation stats query returned no rows.");
|
||||
|
||||
const emailOutboxStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const emailOutboxStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -389,7 +389,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Email outbox stats query returned no rows.");
|
||||
|
||||
const projectPermissionStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const projectPermissionStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -400,7 +400,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Project permission stats query returned no rows.");
|
||||
|
||||
const notificationPreferenceStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const notificationPreferenceStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -411,7 +411,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Notification preference stats query returned no rows.");
|
||||
|
||||
const refreshTokenStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const refreshTokenStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -422,7 +422,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Refresh token stats query returned no rows.");
|
||||
|
||||
const connectedAccountStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const connectedAccountStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -433,7 +433,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Connected account stats query returned no rows.");
|
||||
|
||||
const deletedRowStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
|
||||
const deletedRowStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending",
|
||||
@ -444,7 +444,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
${tenancyWhere}
|
||||
`).at(0) ?? throwErr("Deleted row stats query returned no rows.");
|
||||
|
||||
const deletedRowsByTableRows = await globalPrismaClient.$queryRaw<DeletedRowStatsRow[]>`
|
||||
const deletedRowsByTableRows = await globalPrismaClient.$replica().$queryRaw<DeletedRowStatsRow[]>`
|
||||
SELECT
|
||||
"tableName" AS "table_name",
|
||||
COUNT(*)::bigint AS "total",
|
||||
@ -462,7 +462,7 @@ async function fetchInternalStats(tenancyId: string | null) {
|
||||
? Prisma.sql`AND ("qstashOptions"->'body'->>'tenancyId') = ${tenancyId}`
|
||||
: Prisma.sql``;
|
||||
|
||||
const outgoingStatsRow = (await globalPrismaClient.$queryRaw<OutgoingStatsRow[]>`
|
||||
const outgoingStatsRow = (await globalPrismaClient.$replica().$queryRaw<OutgoingStatsRow[]>`
|
||||
SELECT
|
||||
COUNT(*)::bigint AS "total",
|
||||
COUNT(*) FILTER (WHERE "startedFulfillingAt" IS NULL)::bigint AS "pending",
|
||||
@ -1109,13 +1109,13 @@ export const GET = createSmartRouteHandler({
|
||||
|
||||
const globalStats = shouldIncludeGlobal ? currentStats : null;
|
||||
const globalTenanciesCount = shouldIncludeGlobal
|
||||
? (await globalPrismaClient.$queryRaw<CountRow[]>`
|
||||
? (await globalPrismaClient.$replica().$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS "total"
|
||||
FROM "Tenancy"
|
||||
`).at(0) ?? throwErr("Tenancy count query returned no rows.")
|
||||
: null;
|
||||
const globalDbSyncCount = shouldIncludeGlobal
|
||||
? (await globalPrismaClient.$queryRaw<CountRow[]>`
|
||||
? (await globalPrismaClient.$replica().$queryRaw<CountRow[]>`
|
||||
SELECT COUNT(*)::bigint AS "total"
|
||||
FROM "EnvironmentConfigOverride"
|
||||
WHERE ("config"->'dbSync'->'externalDatabases') IS NOT NULL
|
||||
|
||||
@ -83,7 +83,7 @@ async function assertLocalEmulatorOwnerTeamReadiness() {
|
||||
}
|
||||
|
||||
async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Promise<{ projectId: string, created: boolean }> {
|
||||
const existingRows = await globalPrismaClient.$queryRaw<LocalEmulatorProjectMappingRow[]>(Prisma.sql`
|
||||
const existingRows = await globalPrismaClient.$replica().$queryRaw<LocalEmulatorProjectMappingRow[]>(Prisma.sql`
|
||||
SELECT "projectId"
|
||||
FROM "LocalEmulatorProject"
|
||||
WHERE "absoluteFilePath" = ${absoluteFilePath}
|
||||
@ -187,7 +187,7 @@ async function getOrCreateCredentials(projectId: string) {
|
||||
}
|
||||
|
||||
async function syncLocalEmulatorOnboardingStatus(projectId: string, showOnboarding: boolean): Promise<ProjectOnboardingStatus> {
|
||||
const onboardingStateColumnExistsRows = await globalPrismaClient.$queryRaw<Array<{ exists: boolean }>>(Prisma.sql`
|
||||
const onboardingStateColumnExistsRows = await globalPrismaClient.$replica().$queryRaw<Array<{ exists: boolean }>>(Prisma.sql`
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
@ -198,7 +198,7 @@ async function syncLocalEmulatorOnboardingStatus(projectId: string, showOnboardi
|
||||
`);
|
||||
const onboardingStateColumnExists = onboardingStateColumnExistsRows[0]?.exists === true;
|
||||
|
||||
const rows = await globalPrismaClient.$queryRaw<Array<{ onboardingStatus: string }>>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<Array<{ onboardingStatus: string }>>(Prisma.sql`
|
||||
SELECT "onboardingStatus"
|
||||
FROM "Project"
|
||||
WHERE "id" = ${projectId}
|
||||
@ -385,7 +385,7 @@ export const GET = createSmartRouteHandler({
|
||||
throw new StatusError(StatusError.BadRequest, LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE);
|
||||
}
|
||||
|
||||
const rows = await globalPrismaClient.$queryRaw<LocalEmulatorProjectListRow[]>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<LocalEmulatorProjectListRow[]>(Prisma.sql`
|
||||
SELECT "projectId", "absoluteFilePath", "updatedAt"
|
||||
FROM "LocalEmulatorProject"
|
||||
ORDER BY "updatedAt" DESC
|
||||
|
||||
@ -651,7 +651,7 @@ async function getTransactions(options: {
|
||||
LIMIT ${options.limit + 1}
|
||||
`;
|
||||
|
||||
const rawRows = await options.prisma.$queryRaw<Array<{ rowData: unknown }>>`${Prisma.raw(sql)}`;
|
||||
const rawRows = await options.prisma.$replica().$queryRaw<Array<{ rowData: unknown }>>`${Prisma.raw(sql)}`;
|
||||
const parsedRows = rawRows.map((row) => {
|
||||
const parsed = readLedgerTransactionRow(row.rowData);
|
||||
return {
|
||||
@ -711,7 +711,7 @@ async function getTransactions(options: {
|
||||
FROM (${baseSql}) AS "__rows"
|
||||
WHERE ${refundWhereClauses.join("\n AND ")}
|
||||
`;
|
||||
refundRows = await options.prisma.$queryRaw<Array<{ rowData: unknown }>>`${Prisma.raw(refundSql)}`;
|
||||
refundRows = await options.prisma.$replica().$queryRaw<Array<{ rowData: unknown }>>`${Prisma.raw(refundSql)}`;
|
||||
}
|
||||
const resolvedAdjustedByLookup = buildAdjustedByLookupFromRefundRows(refundRows.map((row) => row.rowData));
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ export async function querySessionReplayAdminRows(options: {
|
||||
suffixSql: Prisma.Sql,
|
||||
}): Promise<SessionReplayAdminListRow[]> {
|
||||
const { prisma, schema, tenancyId, suffixSql } = options;
|
||||
return await prisma.$queryRaw<SessionReplayAdminListRow[]>`
|
||||
return await prisma.$replica().$queryRaw<SessionReplayAdminListRow[]>`
|
||||
SELECT
|
||||
sr."id",
|
||||
sr."projectUserId",
|
||||
|
||||
@ -247,7 +247,7 @@ async function getConversationRow(options: {
|
||||
conversationId: string,
|
||||
viewerProjectUserId?: string,
|
||||
}) {
|
||||
const rows = await globalPrismaClient.$queryRaw<DbConversationRow[]>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<DbConversationRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
c.id AS "conversationId",
|
||||
c."projectUserId" AS "userId",
|
||||
@ -297,7 +297,7 @@ async function getConversationState(options: {
|
||||
conversationId: string,
|
||||
viewerProjectUserId?: string,
|
||||
}) {
|
||||
const rows = await globalPrismaClient.$queryRaw<ConversationStateRow[]>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<ConversationStateRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
c.id AS "conversationId",
|
||||
c."projectUserId" AS "userId",
|
||||
@ -417,7 +417,7 @@ export async function listConversationSummaries(options: {
|
||||
const limit = options.limit ?? 200;
|
||||
const offset = options.offset ?? 0;
|
||||
|
||||
const rows = await globalPrismaClient.$queryRaw<ConversationSummaryRow[]>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<ConversationSummaryRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
c.id AS "conversationId",
|
||||
c."projectUserId" AS "userId",
|
||||
@ -502,7 +502,7 @@ export async function getConversationDetail(options: {
|
||||
throw new StatusError(404, "Conversation not found.");
|
||||
}
|
||||
|
||||
const messageRows = await globalPrismaClient.$queryRaw<ConversationMessageRow[]>(Prisma.sql`
|
||||
const messageRows = await globalPrismaClient.$replica().$queryRaw<ConversationMessageRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
cm.id,
|
||||
cm."messageType",
|
||||
@ -528,7 +528,7 @@ export async function getConversationDetail(options: {
|
||||
const messages = messageRows.map((row) => messageFromRow(row, conversation));
|
||||
const latestMessage = messages.at(-1) ?? throwErr("Conversations must contain at least one message");
|
||||
|
||||
const entryPointRows = await globalPrismaClient.$queryRaw<ConversationEntryPointRow[]>(Prisma.sql`
|
||||
const entryPointRows = await globalPrismaClient.$replica().$queryRaw<ConversationEntryPointRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
cep.id,
|
||||
cep."channelType",
|
||||
|
||||
@ -8,7 +8,7 @@ export const DEVELOPMENT_ENVIRONMENT_ENV_CONFIG_BLOCKED_MESSAGE =
|
||||
export type ConfigOverrideWriteLevel = "project" | "branch" | "environment";
|
||||
|
||||
export async function isDevelopmentEnvironmentProject(projectId: string): Promise<boolean> {
|
||||
const rows = await globalPrismaClient.$queryRaw<Array<{ isDevelopmentEnvironment: boolean }>>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<Array<{ isDevelopmentEnvironment: boolean }>>(Prisma.sql`
|
||||
SELECT "isDevelopmentEnvironment"
|
||||
FROM "Project"
|
||||
WHERE "id" = ${projectId}
|
||||
|
||||
@ -103,7 +103,7 @@ export async function getManagedEmailDomainByTenancyAndSubdomain(options: {
|
||||
tenancyId: string,
|
||||
subdomain: string,
|
||||
}): Promise<ManagedEmailDomain | null> {
|
||||
const rows = await globalPrismaClient.$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
|
||||
SELECT *
|
||||
FROM "ManagedEmailDomain"
|
||||
WHERE "tenancyId" = ${options.tenancyId}
|
||||
@ -117,7 +117,7 @@ export async function getManagedEmailDomainByTenancyAndSubdomain(options: {
|
||||
}
|
||||
|
||||
export async function getManagedEmailDomainByResendDomainId(resendDomainId: string): Promise<ManagedEmailDomain | null> {
|
||||
const rows = await globalPrismaClient.$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
|
||||
SELECT *
|
||||
FROM "ManagedEmailDomain"
|
||||
WHERE "resendDomainId" = ${resendDomainId}
|
||||
@ -216,7 +216,7 @@ export async function markManagedEmailDomainApplied(id: string): Promise<Managed
|
||||
}
|
||||
|
||||
export async function listManagedEmailDomainsForTenancy(tenancyId: string): Promise<ManagedEmailDomain[]> {
|
||||
const rows = await globalPrismaClient.$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
|
||||
const rows = await globalPrismaClient.$replica().$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
|
||||
SELECT *
|
||||
FROM "ManagedEmailDomain"
|
||||
WHERE "tenancyId" = ${tenancyId}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user