Skip to content

Commit d319285

Browse files
authored
Queries view (#1145)
1 parent 907a983 commit d319285

File tree

34 files changed

+3334
-468
lines changed

34 files changed

+3334
-468
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Please review the PR comments with `gh pr status` and fix & resolve those issues that are valid and relevant. Leave those comments that are mostly bullshit unresolved. Report the result to me in detail. Do NOT automatically commit or stage the changes back to the PR!

AGENTS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This file provides guidance to coding agents when working with code in this repo
1212

1313
#### Extra commands
1414
These commands are usually already called by the user, but you can remind them to run it for you if they forgot to.
15-
- **Build packages**: `pnpm build:packages`
15+
- **Build packages**: `pnpm build:packages` (you should never call this yourself)
1616
- **Start dependencies**: `pnpm restart-deps` (resets & restarts Docker containers for DB, Inbucket, etc. Usually already called by the user)
1717
- **Run development**: Already called by the user in the background. You don't need to do this. This will also watch for changes and rebuild packages, codegen, etc. Do NOT call build:packages, dev, codegen, or anything like that yourself, as the dev is already running it.
1818
- **Run minimal dev**: `pnpm dev:basic` (only backend and dashboard for resource-limited systems)
@@ -93,6 +93,9 @@ To see all development ports, refer to the index.html of `apps/dev-launchpad/pub
9393
- If there is an external browser tool connected, use it to test changes you make to the frontend when possible.
9494
- Whenever you update an SDK implementation in `sdks/implementations`, make sure to update the specs accordingly in `sdks/specs` such that if you reimplemented the entire SDK from the specs again, you would get the same implementation. (For example, if the specs are not precise enough to describe a change you made, make the specs more precise.)
9595
- When building internal tools for Stack Auth developers (eg. internal interfaces like the WAL info log etc.): Make the interfaces look very concise, assume the user is a pro-user. This only applies to internal tools that are used primarily by Stack Auth developers.
96+
- The dev server already builds the packages in the background whenever you update a file. If you run into issues with typechecking or linting in a dependency after updating something in a package, just wait a few seconds, and then try again, and they will likely be resolved.
97+
- When asked to review PR comments, you can use `gh pr status` to get the current pull request you're working on.
98+
- NEVER EVER AUTOMATICALLY COMMIT OR STAGE ANY CHANGES — DON'T MODIFY GIT WITHOUT USER CONSENT!
9699
- When building frontend or React code for the dashboard, refer to DESIGN-GUIDE.md.
97100
- NEVER implement a hacky solution without EXPLICIT approval from the user. Always go the extra mile to make sure the solution is clean, maintainable, and robust.
98101
- Fail early, fail loud. Fail fast with an error instead of silently continuing.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
-- Migration to fix incorrectly formatted trusted domain entries in EnvironmentConfigOverride.
2+
--
3+
-- A previous migration sometimes generated entries like:
4+
-- "domains.trustedDomains.<id>.<property1>": value1,
5+
-- "domains.trustedDomains.<id>.<property2>": value2
6+
--
7+
-- Without the parent key:
8+
-- "domains.trustedDomains.<id>": { ... }
9+
--
10+
-- This migration adds an empty object at the <id> level for any missing parent keys:
11+
-- "domains.trustedDomains.<id>": {},
12+
-- "domains.trustedDomains.<id>.<property1>": value1,
13+
-- "domains.trustedDomains.<id>.<property2>": value2
14+
15+
-- Add temporary column to track processed rows (outside transaction so it's visible immediately)
16+
-- SPLIT_STATEMENT_SENTINEL
17+
-- SINGLE_STATEMENT_SENTINEL
18+
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
19+
ALTER TABLE /* SCHEMA_NAME_SENTINEL */."EnvironmentConfigOverride" ADD COLUMN IF NOT EXISTS "temp_trusted_domains_checked" BOOLEAN DEFAULT FALSE;
20+
-- SPLIT_STATEMENT_SENTINEL
21+
22+
-- Create index on the temporary column for efficient querying
23+
-- SPLIT_STATEMENT_SENTINEL
24+
-- SINGLE_STATEMENT_SENTINEL
25+
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
26+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "temp_eco_trusted_domains_checked_idx"
27+
ON /* SCHEMA_NAME_SENTINEL */."EnvironmentConfigOverride" ("temp_trusted_domains_checked")
28+
WHERE "temp_trusted_domains_checked" IS NOT TRUE;
29+
-- SPLIT_STATEMENT_SENTINEL
30+
31+
-- Process rows in batches (outside transaction so each batch commits independently)
32+
-- SPLIT_STATEMENT_SENTINEL
33+
-- SINGLE_STATEMENT_SENTINEL
34+
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
35+
-- CONDITIONALLY_REPEAT_MIGRATION_SENTINEL
36+
WITH rows_to_check AS (
37+
-- Get unchecked rows
38+
SELECT "projectId", "branchId", "config"
39+
FROM /* SCHEMA_NAME_SENTINEL */."EnvironmentConfigOverride"
40+
WHERE "temp_trusted_domains_checked" IS NOT TRUE
41+
-- Keep batch size small for consistent performance
42+
LIMIT 1000
43+
),
44+
matching_keys AS (
45+
-- Find all keys that look like "domains.trustedDomains.<id>.<property...>"
46+
-- (4 or more dot-separated parts starting with domains.trustedDomains)
47+
SELECT
48+
rtc."projectId",
49+
rtc."branchId",
50+
key,
51+
-- Extract the parent key: domains.trustedDomains.<id>
52+
(string_to_array(key, '.'))[1] || '.' ||
53+
(string_to_array(key, '.'))[2] || '.' ||
54+
(string_to_array(key, '.'))[3] AS parent_key
55+
FROM rows_to_check rtc,
56+
jsonb_object_keys(rtc."config") AS key
57+
WHERE key ~ '^domains\.trustedDomains\.[^.]+\..+'
58+
-- Pattern matches: domains.trustedDomains.<id>.<anything>
59+
-- e.g. "domains.trustedDomains.abc123.baseUrl"
60+
),
61+
missing_parents AS (
62+
-- Find parent keys that don't exist in the config
63+
SELECT DISTINCT
64+
mk."projectId",
65+
mk."branchId",
66+
mk.parent_key
67+
FROM matching_keys mk
68+
JOIN rows_to_check rtc
69+
ON rtc."projectId" = mk."projectId"
70+
AND rtc."branchId" = mk."branchId"
71+
WHERE NOT (rtc."config" ? mk.parent_key)
72+
),
73+
parents_to_add AS (
74+
-- Aggregate all missing parent keys per row into a single jsonb object
75+
SELECT
76+
mp."projectId",
77+
mp."branchId",
78+
jsonb_object_agg(mp.parent_key, '{}'::jsonb) AS new_keys
79+
FROM missing_parents mp
80+
GROUP BY mp."projectId", mp."branchId"
81+
),
82+
updated_with_keys AS (
83+
-- Update rows that need new parent keys
84+
UPDATE /* SCHEMA_NAME_SENTINEL */."EnvironmentConfigOverride" eco
85+
SET
86+
"config" = eco."config" || pta.new_keys,
87+
"updatedAt" = NOW(),
88+
"temp_trusted_domains_checked" = TRUE
89+
FROM parents_to_add pta
90+
WHERE eco."projectId" = pta."projectId"
91+
AND eco."branchId" = pta."branchId"
92+
RETURNING eco."projectId", eco."branchId"
93+
),
94+
marked_as_checked AS (
95+
-- Mark all checked rows (including ones that didn't need fixing)
96+
UPDATE /* SCHEMA_NAME_SENTINEL */."EnvironmentConfigOverride" eco
97+
SET "temp_trusted_domains_checked" = TRUE
98+
FROM rows_to_check rtc
99+
WHERE eco."projectId" = rtc."projectId"
100+
AND eco."branchId" = rtc."branchId"
101+
AND NOT EXISTS (
102+
SELECT 1 FROM updated_with_keys uwk
103+
WHERE uwk."projectId" = eco."projectId"
104+
AND uwk."branchId" = eco."branchId"
105+
)
106+
RETURNING eco."projectId"
107+
)
108+
SELECT COUNT(*) > 0 AS should_repeat_migration
109+
FROM rows_to_check;
110+
-- SPLIT_STATEMENT_SENTINEL
111+
112+
-- Clean up: drop temporary index (outside transaction since CREATE was also outside)
113+
-- SPLIT_STATEMENT_SENTINEL
114+
-- SINGLE_STATEMENT_SENTINEL
115+
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
116+
DROP INDEX IF EXISTS /* SCHEMA_NAME_SENTINEL */."temp_eco_trusted_domains_checked_idx";
117+
-- SPLIT_STATEMENT_SENTINEL
118+
119+
-- Clean up: drop temporary column (outside transaction)
120+
-- SPLIT_STATEMENT_SENTINEL
121+
-- SINGLE_STATEMENT_SENTINEL
122+
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
123+
ALTER TABLE /* SCHEMA_NAME_SENTINEL */."EnvironmentConfigOverride" DROP COLUMN IF EXISTS "temp_trusted_domains_checked";

apps/backend/scripts/db-migrations.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { applyMigrations } from "@/auto-migrations";
22
import { MIGRATION_FILES_DIR, getMigrationFiles } from "@/auto-migrations/utils";
33
import { Prisma } from "@/generated/prisma/client";
4+
import { getClickhouseAdminClient } from "@/lib/clickhouse";
45
import { globalPrismaClient, globalPrismaSchema, sqlQuoteIdent } from "@/prisma-client";
56
import { spawnSync } from "child_process";
67
import fs from "fs";
78
import path from "path";
89
import * as readline from "readline";
910
import { seed } from "../prisma/seed";
10-
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
1111
import { runClickhouseMigrations } from "./clickhouse-migrations";
12-
import { getClickhouseAdminClient } from "@/lib/clickhouse";
1312

1413
const getClickhouseClient = () => getClickhouseAdminClient();
1514

@@ -81,7 +80,6 @@ const generateMigrationFile = async () => {
8180
const folderName = `${timestampPrefix()}_${migrationName}`;
8281
const migrationDir = path.join(MIGRATION_FILES_DIR, folderName);
8382
const migrationSqlPath = path.join(migrationDir, 'migration.sql');
84-
const diffUrl = getEnvVariable('STACK_DATABASE_CONNECTION_STRING');
8583

8684
console.log(`Generating migration ${folderName}...`);
8785
const diffResult = spawnSync(
@@ -92,7 +90,6 @@ const generateMigrationFile = async () => {
9290
'migrate',
9391
'diff',
9492
'--from-config-datasource',
95-
diffUrl,
9693
'--to-schema',
9794
'prisma/schema.prisma',
9895
'--script',
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { resetBranchConfigOverrideKeys, resetEnvironmentConfigOverrideKeys } from "@/lib/config";
2+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
3+
import { adaptSchema, adminAuthTypeSchema, yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
4+
5+
const levelSchema = yupString().oneOf(["branch", "environment"]).defined();
6+
7+
const levelConfigs = {
8+
branch: {
9+
reset: (options: { projectId: string, branchId: string, keysToReset: string[] }) =>
10+
resetBranchConfigOverrideKeys(options),
11+
},
12+
environment: {
13+
reset: (options: { projectId: string, branchId: string, keysToReset: string[] }) =>
14+
resetEnvironmentConfigOverrideKeys(options),
15+
},
16+
};
17+
18+
export const POST = createSmartRouteHandler({
19+
metadata: {
20+
hidden: true,
21+
summary: 'Reset config override keys',
22+
description: 'Remove specific keys (and their nested descendants) from the config override at a given level. Uses the same nested key logic as the override algorithm.',
23+
tags: ['Config'],
24+
},
25+
request: yupObject({
26+
auth: yupObject({
27+
type: adminAuthTypeSchema,
28+
tenancy: adaptSchema,
29+
}).defined(),
30+
params: yupObject({
31+
level: levelSchema,
32+
}).defined(),
33+
body: yupObject({
34+
keys: yupArray(yupString().defined()).defined(),
35+
}).defined(),
36+
}),
37+
response: yupObject({
38+
statusCode: yupNumber().oneOf([200]).defined(),
39+
bodyType: yupString().oneOf(["success"]).defined(),
40+
}),
41+
handler: async (req) => {
42+
const levelConfig = levelConfigs[req.params.level];
43+
44+
await levelConfig.reset({
45+
projectId: req.auth.tenancy.project.id,
46+
branchId: req.auth.tenancy.branchId,
47+
keysToReset: req.body.keys,
48+
});
49+
50+
return {
51+
statusCode: 200 as const,
52+
bodyType: "success" as const,
53+
};
54+
},
55+
});

apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { getBranchConfigOverrideQuery, getEnvironmentConfigOverrideQuery, overrideBranchConfigOverride, overrideEnvironmentConfigOverride, setBranchConfigOverride, setBranchConfigOverrideSource, setEnvironmentConfigOverride } from "@/lib/config";
1+
import { getBranchConfigOverrideQuery, getEnvironmentConfigOverrideQuery, overrideBranchConfigOverride, overrideEnvironmentConfigOverride, setBranchConfigOverride, setBranchConfigOverrideSource, setEnvironmentConfigOverride, validateBranchConfigOverride, validateEnvironmentConfigOverride } from "@/lib/config";
22
import { enqueueExternalDbSync } from "@/lib/external-db-sync-queue";
33
import { globalPrismaClient, rawQuery } from "@/prisma-client";
44
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
55
import { branchConfigSchema, environmentConfigSchema, getConfigOverrideErrors, migrateConfigOverride } from "@stackframe/stack-shared/dist/config/schema";
66
import { adaptSchema, adminAuthTypeSchema, branchConfigSourceSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
7-
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
7+
import { StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
88
import * as yup from "yup";
99

1010
type BranchConfigSourceApi = yup.InferType<typeof branchConfigSourceSchema>;
@@ -50,6 +50,11 @@ const levelConfigs = {
5050
branchId: options.branchId,
5151
branchConfigOverrideOverride: options.config,
5252
}),
53+
validate: (options: { projectId: string, branchId: string, config: any }) =>
54+
validateBranchConfigOverride({
55+
projectId: options.projectId,
56+
branchConfigOverride: options.config,
57+
}),
5358
requiresSource: true,
5459
},
5560
environment: {
@@ -69,6 +74,12 @@ const levelConfigs = {
6974
branchId: options.branchId,
7075
environmentConfigOverrideOverride: options.config,
7176
}),
77+
validate: (options: { projectId: string, branchId: string, config: any }) =>
78+
validateEnvironmentConfigOverride({
79+
projectId: options.projectId,
80+
branchId: options.branchId,
81+
environmentConfigOverride: options.config,
82+
}),
7283
requiresSource: false,
7384
},
7485
};
@@ -141,6 +152,20 @@ async function parseAndValidateConfig(
141152
return migratedConfig;
142153
}
143154

155+
async function warnOnValidationFailure(
156+
levelConfig: typeof levelConfigs["branch" | "environment"],
157+
options: { projectId: string, branchId: string, config: any },
158+
) {
159+
try {
160+
const validationResult = await levelConfig.validate(options);
161+
if (validationResult.status === "error") {
162+
captureError("config-override-validation-warning", `Config override validation warning for project ${options.projectId} (this may not be a logic error, but rather a client/implementation issue — e.g. dot notation into non-existent record entries): ${validationResult.error}`);
163+
}
164+
} catch (e) {
165+
captureError("config-override-validation-check-failed", e);
166+
}
167+
}
168+
144169
export const PUT = createSmartRouteHandler({
145170
metadata: {
146171
hidden: true,
@@ -179,6 +204,12 @@ export const PUT = createSmartRouteHandler({
179204
source: req.body.source as BranchConfigSourceApi,
180205
});
181206

207+
await warnOnValidationFailure(levelConfig, {
208+
projectId: req.auth.tenancy.project.id,
209+
branchId: req.auth.tenancy.branchId,
210+
config: parsedConfig,
211+
});
212+
182213
if (req.params.level === "environment" && shouldEnqueueExternalDbSync(parsedConfig)) {
183214
await enqueueExternalDbSync(req.auth.tenancy.id);
184215
}
@@ -220,6 +251,12 @@ export const PATCH = createSmartRouteHandler({
220251
config: parsedConfig,
221252
});
222253

254+
await warnOnValidationFailure(levelConfig, {
255+
projectId: req.auth.tenancy.project.id,
256+
branchId: req.auth.tenancy.branchId,
257+
config: parsedConfig,
258+
});
259+
223260
if (req.params.level === "environment" && shouldEnqueueExternalDbSync(parsedConfig)) {
224261
await enqueueExternalDbSync(req.auth.tenancy.id);
225262
}

apps/backend/src/auto-migrations/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export async function applyMigrations(options: {
132132
}
133133

134134
for (const statementRaw of migration.sql.split('SPLIT_STATEMENT_SENTINEL')) {
135-
const statement = statementRaw.replace('/* SCHEMA_NAME_SENTINEL */', sqlQuoteIdentToString(options.schema));
135+
const statement = statementRaw.replaceAll('/* SCHEMA_NAME_SENTINEL */', sqlQuoteIdentToString(options.schema));
136136
const runOutside = statement.includes('RUN_OUTSIDE_TRANSACTION_SENTINEL');
137137
const isSingleStatement = statement.includes('SINGLE_STATEMENT_SENTINEL');
138138
const isConditionallyRepeatMigration = statement.includes('CONDITIONALLY_REPEAT_MIGRATION_SENTINEL');

0 commit comments

Comments
 (0)