Merge branch 'dev' into project-config-to-json

This commit is contained in:
Zai Shi 2025-08-01 00:56:34 +02:00 committed by GitHub
commit 5d451709b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 20 additions and 11 deletions

View File

@ -1,6 +1,6 @@
import { listPermissions } from "@/lib/permissions";
import { Tenancy } from "@/lib/tenancies";
import { getPrismaClientForTenancy } from "@/prisma-client";
import { getPrismaClientForTenancy, retryTransaction } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { SmartRequestAuth } from "@/route-handlers/smart-request";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
@ -57,7 +57,7 @@ async function ensureUserCanManageApiKeys(
// Check team API key permissions
if (options.teamId !== undefined) {
const userId = auth.user.id;
const hasManageApiKeysPermission = await prisma.$transaction(async (tx) => {
const hasManageApiKeysPermission = await retryTransaction(prisma, async (tx) => {
const permissions = await listPermissions(tx, {
scope: 'team',
tenancy: auth.tenancy,

View File

@ -1,7 +1,7 @@
import { getSharedEmailConfig, sendEmail } from "@/lib/emails";
import { listPermissions } from "@/lib/permissions";
import { getTenancy } from "@/lib/tenancies";
import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client";
import { getPrismaClientForTenancy, globalPrismaClient, retryTransaction } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { KnownErrors } from "@stackframe/stack-shared";
import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
@ -27,7 +27,7 @@ export const POST = createSmartRouteHandler({
async handler({ body }) {
// Get the API key and revoke it. We use a transaction to ensure we do not send emails multiple times.
// We don't support revoking API keys in tenancies with non-global source of truth atm.
const updatedApiKey = await globalPrismaClient.$transaction(async (tx) => {
const updatedApiKey = await retryTransaction(globalPrismaClient, async (tx) => {
// Find the API key in the database
const apiKey = await tx.projectApiKey.findUnique({
where: {
@ -116,7 +116,7 @@ export const POST = createSmartRouteHandler({
const prisma = await getPrismaClientForTenancy(tenancy);
const userIdsWithManageApiKeysPermission = await prisma.$transaction(async (tx) => {
const userIdsWithManageApiKeysPermission = await retryTransaction(prisma, async (tx) => {
if (!updatedApiKey.teamId) {
throw new StackAssertionError("Team ID not specified in team API key");
}

View File

@ -33,7 +33,7 @@ export const POST = createSmartRouteHandler({
const set = await globalPrismaClient.apiKeySet.create({
data: {
projectId: req.body.project_id,
description: `Auto-generated for ${req.body.external_project_name ? `"${req.body.external_project_name}"` : "an external project"}`,
description: `Auto-generated for ${req.body.external_project_name ? `"${req.body.external_project_name}"` : "an external project"} (DO NOT DELETE)`,
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100),
superSecretAdminKey: `sak_${generateSecureRandomString()}`,
},

View File

@ -33,7 +33,7 @@ export const POST = createSmartRouteHandler({
const set = await globalPrismaClient.apiKeySet.create({
data: {
projectId: req.body.project_id,
description: `Auto-generated for ${req.body.external_project_name ? `"${req.body.external_project_name}"` : "an external project"}`,
description: `Auto-generated for ${req.body.external_project_name ? `"${req.body.external_project_name}"` : "an external project"} (DO NOT DELETE)`,
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100),
superSecretAdminKey: `sak_${generateSecureRandomString()}`,
},

View File

@ -69,7 +69,7 @@ export const POST = createSmartRouteHandler({
const set = await createApiKeySet({
projectId: createdProject.id,
description: `Auto-generated for Neon (${req.body.display_name})`,
description: `Auto-generated for Neon Auth (DO NOT DELETE)`,
expires_at_millis: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100).getTime(),
has_publishable_client_key: false,
has_secret_server_key: false,

View File

@ -285,6 +285,7 @@ import.meta.vitest?.test("applies migration while running concurrent queries", r
}));
import.meta.vitest?.test("applies migration while running an interactive transaction", runTest(async ({ expect, prismaClient, dbURL }) => {
// eslint-disable-next-line no-restricted-syntax
return await prismaClient.$transaction(async (tx, ...args) => {
await runMigrationNeeded({
prismaClient,
@ -304,6 +305,7 @@ import.meta.vitest?.test("applies migration while running an interactive transac
import.meta.vitest?.test("applies migration while running concurrent interactive transactions", runTest(async ({ expect, prismaClient, dbURL }) => {
const runTransactionWithMigration = async (testValue: string) => {
// eslint-disable-next-line no-restricted-syntax
return await prismaClient.$transaction(async (tx) => {
await runMigrationNeeded({
prismaClient,

View File

@ -39,6 +39,7 @@ async function getAppliedMigrations(options: {
prismaClient: PrismaClient,
schema: string,
}) {
// eslint-disable-next-line no-restricted-syntax
const [_1, _2, _3, appliedMigrations] = await options.prismaClient.$transaction([
options.prismaClient.$executeRaw`SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_ID})`,
options.prismaClient.$executeRaw(Prisma.sql`
@ -143,6 +144,7 @@ export async function applyMigrations(options: {
VALUES (${migration.migrationName}, clock_timestamp())
`);
try {
// eslint-disable-next-line no-restricted-syntax
await options.prismaClient.$transaction(transaction);
} catch (e) {
const error = getMigrationError(e);

View File

@ -234,7 +234,7 @@ export async function createOrUpdateProjectWithLegacyConfig(
// Update owner metadata
const internalEnvironmentConfig = await rawQuery(globalPrismaClient, getRenderedEnvironmentConfigQuery({ projectId: "internal", branchId: DEFAULT_BRANCH_ID }));
const prisma = await getPrismaClientForSourceOfTruth(internalEnvironmentConfig.sourceOfTruth, DEFAULT_BRANCH_ID);
await prisma.$transaction(async (tx) => {
await retryTransaction(prisma, async (tx) => {
for (const userId of options.ownerIds ?? []) {
const projectUserTx = await tx.projectUser.findUnique({
where: {

View File

@ -129,6 +129,7 @@ export async function retryTransaction<T>(client: PrismaClient, fn: (tx: PrismaC
return await traceSpan(`transaction attempt #${attemptIndex}`, async (attemptSpan) => {
const attemptRes = await (async () => {
try {
// eslint-disable-next-line no-restricted-syntax
return Result.ok(await client.$transaction(async (tx, ...args) => {
let res;
try {

View File

@ -233,7 +233,7 @@ it(`should exchange the authorization code for an admin API key that works`, asy
"items": [
{
"created_at_millis": <stripped field 'created_at_millis'>,
"description": "Auto-generated for an external project",
"description": "Auto-generated for an external project (DO NOT DELETE)",
"expires_at_millis": <stripped field 'expires_at_millis'>,
"id": "<stripped UUID>",
"super_secret_admin_key": { "last_four": <stripped field 'last_four'> },

View File

@ -233,7 +233,7 @@ it(`should exchange the authorization code for an admin API key that works`, asy
"items": [
{
"created_at_millis": <stripped field 'created_at_millis'>,
"description": "Auto-generated for an external project",
"description": "Auto-generated for an external project (DO NOT DELETE)",
"expires_at_millis": <stripped field 'expires_at_millis'>,
"id": "<stripped UUID>",
"super_secret_admin_key": { "last_four": <stripped field 'last_four'> },

View File

@ -103,6 +103,10 @@ module.exports = {
"Identifier[name='localeCompare']",
message: "Use stringCompare() from utils/strings.tsx instead of String.prototype.localeCompare.",
},
{
selector: "CallExpression > MemberExpression[property.name='$transaction']",
message: "Calling .$transaction is disallowed. Use retryTransaction() instead.",
},
],
"@typescript-eslint/no-misused-promises": [
"error",