From 4295bcb8454d44f162eab2daf49fd26e1ecc6925 Mon Sep 17 00:00:00 2001 From: soimon Date: Fri, 14 Feb 2025 00:04:35 +0100 Subject: [PATCH 01/18] Docs: Fix a small error in an example (#33608) Fix an error in the example It says it will declare 6 test cases, but it declares 4. --- apps/docs/content/guides/local-development/testing/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/local-development/testing/overview.mdx b/apps/docs/content/guides/local-development/testing/overview.mdx index eca62644d1c..f0fb1070f90 100644 --- a/apps/docs/content/guides/local-development/testing/overview.mdx +++ b/apps/docs/content/guides/local-development/testing/overview.mdx @@ -54,7 +54,7 @@ This example demonstrates setting up and testing RLS policies for a simple todo -- install tests utilities -- install pgtap extension for testing create extension if not exists pgtap with schema extensions; - -- Start declare we'll have 6 test cases in our test suite + -- Start declare we'll have 4 test cases in our test suite select plan(4); -- Setup our testing data From 2a3a2a597ea76c5175d5da76a148bb22ce12bc9a Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Fri, 14 Feb 2025 08:06:16 +0100 Subject: [PATCH 02/18] Add E2E tests for local studio logs (#33592) * add e2e tests for local studio logs * fix auth schema test * comment out log pages without logs * fix tests rm react query from dom, it blocks view of definition button * undo auth table changes --- .../DefaultPreviewSelectionRenderer.tsx | 21 ++++- tests/local-studio-tests/playwright.config.ts | 7 +- .../tests/snapshot/spec/logs.spec.ts | 81 +++++++++++++++++++ .../tests/snapshot/spec/table-editor.spec.ts | 22 ++++- 4 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 tests/local-studio-tests/tests/snapshot/spec/logs.spec.ts diff --git a/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx b/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx index d4d8792c224..08c5a7f281b 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx @@ -16,7 +16,15 @@ import { useState, useEffect } from 'react' const LogRowSeparator = () => -const PropertyRow = ({ keyName, value }: { keyName: string; value: any }) => { +const PropertyRow = ({ + keyName, + value, + dataTestId, +}: { + keyName: string + value: any + dataTestId?: string +}) => { const isTimestamp = keyName === 'timestamp' || keyName === 'created_at' || keyName === 'updated_at' @@ -83,7 +91,7 @@ const PropertyRow = ({ keyName, value }: { keyName: string; value: any }) => { return ( - +
{ const { timestamp, event_message, metadata, id, ...rest } = log return ( -
+
{log?.id && } {log?.timestamp && ( - + )} {log?.event_message && ( diff --git a/tests/local-studio-tests/playwright.config.ts b/tests/local-studio-tests/playwright.config.ts index 2568a2cdcee..b3a260763ef 100644 --- a/tests/local-studio-tests/playwright.config.ts +++ b/tests/local-studio-tests/playwright.config.ts @@ -30,13 +30,18 @@ export default defineConfig({ trace: 'on-first-retry', // record a video for failed tests, but only for local testing. We can't store videos on CI atm. video: 'retain-on-failure', + launchOptions: { + env: { + NODE_ENV: 'test', + }, + }, }, /* Configure projects for major browsers */ projects: [ { name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + use: { ...devices['Desktop Chrome'], viewport: { width: 1366, height: 768 } }, }, // { diff --git a/tests/local-studio-tests/tests/snapshot/spec/logs.spec.ts b/tests/local-studio-tests/tests/snapshot/spec/logs.spec.ts new file mode 100644 index 00000000000..ab7c8f5b37b --- /dev/null +++ b/tests/local-studio-tests/tests/snapshot/spec/logs.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test' + +const LOGS_PAGES = [ + 'API Gateway', + 'Postgres', + 'PostgREST', + // 'Auth', Wont have logs on first load + // 'Storage', Wont have logs on first load + //'Realtime' Wont have logs on first load +] + +test.describe('Logs', async () => { + for (const logPage of LOGS_PAGES) { + test.describe(`${logPage} logs page`, () => { + test('can navigate to logs page', async ({ page }) => { + await page.goto('http://localhost:8082/project/default') + await page.locator('a', { hasText: 'Logs' }).click({ timeout: 4000 }) + await expect(page.getByText('Logs & Analytics')).toBeVisible() + + // Click anywhere on the screen to close the sidebar + await page.click('body') + + await page + .getByRole('link', { name: logPage, exact: true }) + .click() + .catch((e) => { + console.log('🔴 Error clicking', logPage, e) + throw e + }) + + // Wait for and verify the logs table is present + const logsTable = page.getByRole('table') + await expect(logsTable).toBeVisible() + }) + + test('shows logs data without errors', async ({ page }) => { + // Navigate to page first + await page.goto('http://localhost:8082/project/default') + await page.locator('a', { hasText: 'Logs' }).click({ timeout: 4000 }) + await page.click('body') + await page.getByRole('link', { name: logPage, exact: true }).click() + + // Wait a bit and check for errors with a longer timeout + const error = page.getByText('Error fetching logs') + await expect(error).not.toBeVisible({ timeout: 10000 }) + + // Check if the logs table has any rows + const gridcells = page.getByRole('gridcell') + await expect(gridcells.first()).toBeVisible() + }) + + test('can select and view log details', async ({ page }) => { + // Navigate to page first + await page.goto('http://localhost:8082/project/default') + await page.locator('a', { hasText: 'Logs' }).click({ timeout: 4000 }) + await page.click('body') + await page.getByRole('link', { name: logPage, exact: true }).click() + + const gridcells = page.getByRole('gridcell') + + // Click first row and verify details + await gridcells.first().click() + const tabPanel = page.getByTestId('log-selection') + await expect(tabPanel).toBeVisible({ timeout: 2000 }) + + const selectionPanelTimestamp = tabPanel.getByTestId('log-selection-timestamp') + await expect(selectionPanelTimestamp).toBeVisible() + + const rawTimestamp = await selectionPanelTimestamp.textContent() + const timestamp = rawTimestamp?.replace('timestamp', '') + const rowText = await gridcells.first().textContent() + expect(rowText).toContain(timestamp) + + // Click second row and verify different content + await gridcells.nth(1).click() + const tabPanelText2 = await tabPanel.textContent() + expect(tabPanelText2).not.toBe(rowText) + }) + }) + } +}) diff --git a/tests/local-studio-tests/tests/snapshot/spec/table-editor.spec.ts b/tests/local-studio-tests/tests/snapshot/spec/table-editor.spec.ts index 6323716141f..a689cdcbf90 100644 --- a/tests/local-studio-tests/tests/snapshot/spec/table-editor.spec.ts +++ b/tests/local-studio-tests/tests/snapshot/spec/table-editor.spec.ts @@ -19,7 +19,7 @@ test.describe('Table Editor page', () => { test('should create a new table, view its definition, add new rows, sort and filter', async ({ page, }, testInfo) => { - const tableName = `${kebabCase(testInfo.title).slice(0, 24)}-${testInfo.retry}-${Math.floor(Math.random() * 100)}` + const tableName = `${kebabCase(testInfo.title).slice(0, 24)}-${testInfo.retry}-${Math.floor(Math.random() * 10000)}` // The page has been loaded with the table data, we can now interact with the page await page.getByRole('button', { name: 'New table', exact: true }).click() @@ -46,6 +46,14 @@ test.describe('Table Editor page', () => { await page.getByRole('button', { name: 'Save' }).click() await dismissToast(page) + // hide React Query DevTools if present + await page.evaluate(() => { + const devtools = document.querySelector('.ReactQueryDevtools') + if (devtools) { + devtools.remove() + } + }) + // view its definition await page.getByText('definition').click() await expect(page.locator('div.view-lines')).toContainText( @@ -69,9 +77,17 @@ test.describe('Table Editor page', () => { await page.getByTestId('action-bar-save-row').click() await dismissToast(page) + // Wait for both rows to be visible in the grid + await page.waitForResponse((response) => + response.url().includes('/api/platform/pg-meta/default/query') + ) await expect(page.getByRole('grid')).toContainText('2') await expect(page.getByRole('grid')).toContainText('100') + // Make sure we can see both rows in the grid before sorting + const rows = page.getByRole('row') + await expect(rows).toHaveCount(3) // header row + 2 data rows + // sort by the a column await page.getByRole('button', { name: 'Sort' }).click() await page.getByTestId('table-editor-pick-column-to-sort-button').click() @@ -82,8 +98,8 @@ test.describe('Table Editor page', () => { await page.locator('#spec-click-target').click() // expect the row to be sorted by defaultValueColumn. They're inserted in the order 100, 2 - await expect(page.locator('div.rdg-row:nth-child(2)')).toContainText('2') - await expect(page.locator('div.rdg-row:nth-child(3)')).toContainText('100') + await expect(rows.nth(1)).toContainText('2') + await expect(rows.nth(2)).toContainText('100') // remove the sorting await page.getByRole('button', { name: 'Sorted by 1 rule' }).click() await page.getByRole('dialog').getByRole('button').nth(1).click() From 0106c4b319fed026dd867cc6746536c2f31a1743 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Fri, 14 Feb 2025 16:11:07 +0900 Subject: [PATCH 03/18] chore(pg-meta): port functions tests (#33480) * chore(pg-meta): port functions tests --- packages/pg-meta/package.json | 4 +- packages/pg-meta/src/pg-meta-functions.ts | 22 +- packages/pg-meta/src/sql/functions.ts | 7 +- packages/pg-meta/test/db/00-init.sql | 3 +- packages/pg-meta/test/db/01-memes.sql | 5 +- packages/pg-meta/test/db/utils.ts | 59 ++-- packages/pg-meta/test/functions.test.ts | 343 ++++++++++++++++++++++ packages/pg-meta/tsconfig.json | 5 +- pnpm-lock.yaml | 198 ++++++++++--- 9 files changed, 564 insertions(+), 82 deletions(-) create mode 100644 packages/pg-meta/test/functions.test.ts diff --git a/packages/pg-meta/package.json b/packages/pg-meta/package.json index a07e57a7a83..cb9ab3074fa 100644 --- a/packages/pg-meta/package.json +++ b/packages/pg-meta/package.json @@ -17,8 +17,10 @@ "zod": "^3.22.4" }, "devDependencies": { + "@types/pg": "^8.11.11", "npm-run-all": "^4.1.5", - "postgres": "^3.4.5", + "pg": "^8.13.1", + "postgres-array": "^3.0.2", "typescript": "~5.5.0", "vitest": "^2.1.8" } diff --git a/packages/pg-meta/src/pg-meta-functions.ts b/packages/pg-meta/src/pg-meta-functions.ts index 7af10c36d65..db65046f5c5 100644 --- a/packages/pg-meta/src/pg-meta-functions.ts +++ b/packages/pg-meta/src/pg-meta-functions.ts @@ -36,11 +36,11 @@ export const pgFunctionZod = z.object({ config_params: z.union([z.record(z.string(), z.string()), z.null()]), }) +export type PGFunction = z.infer + export const pgFunctionArrayZod = z.array(pgFunctionZod) export const pgFunctionOptionalZod = z.optional(pgFunctionZod) -export type PGFunction = z.infer - export function list({ includeSystemSchemas = false, includedSchemas, @@ -86,10 +86,12 @@ export function list({ } } -export function retrieve({ id }: { id: number }): { +type FunctionsRetrieveReturn = { sql: string zod: typeof pgFunctionOptionalZod } + +export function retrieve({ id }: { id: number }): FunctionsRetrieveReturn export function retrieve({ name, schema, @@ -98,10 +100,7 @@ export function retrieve({ name: string schema: string args: string[] -}): { - sql: string - zod: typeof pgFunctionOptionalZod -} +}): FunctionsRetrieveReturn export function retrieve({ id, name, @@ -112,10 +111,7 @@ export function retrieve({ name?: string schema?: string args?: string[] -}): { - sql: string - zod: typeof pgFunctionOptionalZod -} { +}): FunctionsRetrieveReturn { if (id) { const sql = /* SQL */ ` with f as ( @@ -212,8 +208,8 @@ function _generateCreateFunctionSql( config_params ? Object.entries(config_params) .map( - ([param, value]: string[]) => - `SET ${param} ${value[0] === 'FROM CURRENT' ? 'FROM CURRENT' : 'TO ' + (value === '""' ? "''" : value)}` + ([param, value]) => + `SET ${param} ${value === 'FROM CURRENT' ? 'FROM CURRENT' : 'TO ' + (value === '""' ? "''" : value)}` ) .join('\n') : '' diff --git a/packages/pg-meta/src/sql/functions.ts b/packages/pg-meta/src/sql/functions.ts index a2cbe04ee8f..996d13fd253 100644 --- a/packages/pg-meta/src/sql/functions.ts +++ b/packages/pg-meta/src/sql/functions.ts @@ -80,7 +80,8 @@ from 'mode', t2.mode, 'name', name, 'type_id', type_id, - 'has_default', has_default + -- Cast null into false boolean + 'has_default', COALESCE(has_default, false) )) as args from ( @@ -88,7 +89,9 @@ from oid, unnest(arg_modes) as mode, unnest(arg_names) as name, - unnest(arg_types) as type_id, + -- Coming from: coalesce(p.proallargtypes, p.proargtypes) postgres won't automatically assume + -- integer, we need to cast it to be properly parsed + unnest(arg_types)::int8 as type_id, unnest(arg_has_defaults) as has_default from functions diff --git a/packages/pg-meta/test/db/00-init.sql b/packages/pg-meta/test/db/00-init.sql index e28a0b165df..5f7cc9a9c4d 100644 --- a/packages/pg-meta/test/db/00-init.sql +++ b/packages/pg-meta/test/db/00-init.sql @@ -1,4 +1,5 @@ - +-- We apply our test seedings to template1 so every new created db will have the same structure +\c template1 -- Tables for testing diff --git a/packages/pg-meta/test/db/01-memes.sql b/packages/pg-meta/test/db/01-memes.sql index d4909b7abf9..a9671e0feb8 100644 --- a/packages/pg-meta/test/db/01-memes.sql +++ b/packages/pg-meta/test/db/01-memes.sql @@ -1,4 +1,5 @@ - +-- We apply our test seedings to template1 so every new created db will have the same structure +\c template1 CREATE TABLE public.category ( id serial NOT NULL PRIMARY KEY, @@ -2431,3 +2432,5 @@ INSERT INTO public.memes (name, category, created_at) VALUES ('Drinking Out of Cups', 4, NOW()), ('Sonic the Hedgehog (2020 Film)', 4, NOW()), ('Art Student Owl', 4, NOW()); + +\c postgres \ No newline at end of file diff --git a/packages/pg-meta/test/db/utils.ts b/packages/pg-meta/test/db/utils.ts index 489a10d6455..c5e205d2574 100644 --- a/packages/pg-meta/test/db/utils.ts +++ b/packages/pg-meta/test/db/utils.ts @@ -1,32 +1,57 @@ -import postgres from 'postgres' +import pg, { Pool } from 'pg' import { randomUUID } from 'crypto' +import { parse as parseArray } from 'postgres-array' + +// Those types override are in sync with `postgres-meta` since the queries +// will get executed via `execQuery` on a pg connection with the same configuration +// see: https://github.com/supabase/postgres-meta/blob/ca06061b4708971628134f95e49f254c2dfdfa7d/src/lib/db.ts#L6-L23 +pg.types.setTypeParser(pg.types.builtins.INT8, (x) => { + const asNumber = Number(x) + if (Number.isSafeInteger(asNumber)) { + return asNumber + } else { + return x + } +}) +pg.types.setTypeParser(pg.types.builtins.DATE, (x) => x) +pg.types.setTypeParser(pg.types.builtins.INTERVAL, (x) => x) +pg.types.setTypeParser(pg.types.builtins.TIMESTAMP, (x) => x) +pg.types.setTypeParser(pg.types.builtins.TIMESTAMPTZ, (x) => x) +pg.types.setTypeParser(1115, parseArray) // _timestamp +pg.types.setTypeParser(1182, parseArray) // _date +pg.types.setTypeParser(1185, parseArray) // _timestamptz +pg.types.setTypeParser(600, (x) => x) // point +pg.types.setTypeParser(1017, (x) => x) // _point const ROOT_DB_URL = process.env.DATABASE_URL ?? 'postgresql://postgres:postgres@localhost:5432' const ROOT_DB_NAME = process.env.DATABASE_NAME ?? 'postgres' -// Connection to the postgres server (not a specific database) -const rootDb = postgres(`${ROOT_DB_URL}/${ROOT_DB_NAME}`) + +// Replace postgres.js root connection with pg connection +const rootPool = new Pool({ + connectionString: `${ROOT_DB_URL}/${ROOT_DB_NAME}`, + max: 1, +}) export async function createTestDatabase() { const dbName = `test_${randomUUID().replace(/-/g, '_')}` try { - // Create a new database for this test - await rootDb.unsafe(`CREATE DATABASE ${dbName};`) + await rootPool.query(`CREATE DATABASE ${dbName};`) - // Create a new connection to the test database - const testDb = postgres(`${ROOT_DB_URL}/${dbName}`, { + const pool = new Pool({ + connectionString: `${ROOT_DB_URL}/${dbName}`, max: 1, - idle_timeout: 20, - connect_timeout: 10, + idleTimeoutMillis: 20000, + connectionTimeoutMillis: 10000, }) - // Return both the database name and the client return { dbName, + client: 'pg' as const, executeQuery: async (query: string): Promise => { try { - const result = (await testDb.unsafe(query)).map((v) => v) - return result as T + const res = await pool.query(query) + return res.rows as T } catch (error) { if (error instanceof Error) { throw new Error(`Failed to execute query: ${error.message}`) @@ -35,10 +60,8 @@ export async function createTestDatabase() { } }, cleanup: async () => { - // Close the test database connection - await testDb.end() - // Drop the test database - await rootDb.unsafe(`DROP DATABASE ${dbName};`) + await pool.end() + await rootPool.query(`DROP DATABASE ${dbName};`) }, } } catch (error) { @@ -49,7 +72,7 @@ export async function createTestDatabase() { } } -// Cleanup the root connection when all tests are done +// Update cleanup function to use pg pool export async function cleanupRoot() { - await rootDb.end() + await rootPool.end() } diff --git a/packages/pg-meta/test/functions.test.ts b/packages/pg-meta/test/functions.test.ts new file mode 100644 index 00000000000..82729891915 --- /dev/null +++ b/packages/pg-meta/test/functions.test.ts @@ -0,0 +1,343 @@ +import { expect, test, beforeAll, afterAll } from 'vitest' +import pgMeta from '../src/index' +import { createTestDatabase, cleanupRoot } from './db/utils' + +beforeAll(async () => { + // Any global setup if needed +}) + +afterAll(async () => { + await cleanupRoot() +}) + +const withTestDatabase = ( + name: string, + fn: (db: Awaited>) => Promise +) => { + test(name, async () => { + const db = await createTestDatabase() + try { + await fn(db) + } finally { + await db.cleanup() + } + }) +} + +withTestDatabase('list functions', async ({ executeQuery }) => { + const { sql, zod } = await pgMeta.functions.list() + const res = zod.parse(await executeQuery(sql)) + + // Test for the 'add' function created in init.sql + const addFunction = res.find(({ name }) => name === 'add') + expect(addFunction).toMatchInlineSnapshot( + { id: expect.any(Number) }, + ` + { + "args": [ + { + "has_default": false, + "mode": "in", + "name": "", + "type_id": 23, + }, + { + "has_default": false, + "mode": "in", + "name": "", + "type_id": 23, + }, + ], + "argument_types": "integer, integer", + "behavior": "IMMUTABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION public.add(integer, integer) + RETURNS integer + LANGUAGE sql + IMMUTABLE STRICT + AS $function$select $1 + $2;$function$ + ", + "config_params": null, + "definition": "select $1 + $2;", + "id": Any, + "identity_argument_types": "integer, integer", + "is_set_returning_function": false, + "language": "sql", + "name": "add", + "return_type": "integer", + "return_type_id": 23, + "return_type_relation_id": null, + "schema": "public", + "security_definer": false, + } + ` + ) +}) + +withTestDatabase('list functions with included schemas', async ({ executeQuery }) => { + const { sql, zod } = await pgMeta.functions.list({ + includedSchemas: ['public'], + }) + const res = zod.parse(await executeQuery(sql)) + + expect(res.length).toBeGreaterThan(0) + res.forEach((func) => { + expect(func.schema).toBe('public') + }) +}) + +withTestDatabase('list functions with excluded schemas', async ({ executeQuery }) => { + const { sql, zod } = await pgMeta.functions.list({ + excludedSchemas: ['public'], + }) + const res = zod.parse(await executeQuery(sql)) + + res.forEach((func) => { + expect(func.schema).not.toBe('public') + }) +}) + +withTestDatabase( + 'list functions with excluded schemas and include System Schemas', + async ({ executeQuery }) => { + const { sql, zod } = await pgMeta.functions.list({ + excludedSchemas: ['public'], + includeSystemSchemas: true, + }) + const res = zod.parse(await executeQuery(sql)) + + expect(res.length).toBeGreaterThan(0) + res.forEach((func) => { + expect(func.schema).not.toBe('public') + }) + } +) + +withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => { + // Create function + const { sql: createSql } = await pgMeta.functions.create({ + name: 'test_func', + schema: 'public', + args: ['a int2', 'b int2'], + definition: 'select a + b', + return_type: 'integer', + language: 'sql', + behavior: 'STABLE', + security_definer: true, + config_params: { search_path: 'hooks, auth', role: 'postgres' }, + }) + await executeQuery(createSql) + + // // Retrieve function + const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.functions.retrieve({ + name: 'test_func', + schema: 'public', + args: ['a int2', 'b int2'], + }) + const retrieve = await executeQuery(retrieveSql) + const res = retrieveZod.parse(retrieve[0]) + const functionId = res!.id + expect({ data: res, error: null }).toMatchInlineSnapshot( + { data: { id: expect.any(Number) } }, + ` + { + "data": { + "args": [ + { + "has_default": false, + "mode": "in", + "name": "a", + "type_id": 21, + }, + { + "has_default": false, + "mode": "in", + "name": "b", + "type_id": 21, + }, + ], + "argument_types": "a smallint, b smallint", + "behavior": "STABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION public.test_func(a smallint, b smallint) + RETURNS integer + LANGUAGE sql + STABLE SECURITY DEFINER + SET search_path TO 'hooks', 'auth' + SET role TO 'postgres' + AS $function$select a + b$function$ + ", + "config_params": { + "role": "postgres", + "search_path": "hooks, auth", + }, + "definition": "select a + b", + "id": Any, + "identity_argument_types": "a smallint, b smallint", + "is_set_returning_function": false, + "language": "sql", + "name": "test_func", + "return_type": "integer", + "return_type_id": 23, + "return_type_relation_id": null, + "schema": "public", + "security_definer": true, + }, + "error": null, + } + ` + ) + // create test_schema to move the function into: + const { sql: createSchemaSql } = await pgMeta.schemas.create({ name: 'test_schema' }) + await executeQuery(createSchemaSql) + const { sql: updateSql } = await pgMeta.functions.update(res!, { + name: 'test_func_renamed', + schema: 'test_schema', + definition: 'select b - a', + }) + await executeQuery(updateSql) + + const { sql: retrieveRenamedSql } = await pgMeta.functions.retrieve({ id: functionId }) + const retrieveRenamed = await executeQuery(retrieveRenamedSql) + const resUpdated = retrieveZod.parse(retrieveRenamed[0]) + expect({ data: resUpdated, error: null }).toMatchInlineSnapshot( + { data: { id: expect.any(Number) } }, + ` + { + "data": { + "args": [ + { + "has_default": false, + "mode": "in", + "name": "a", + "type_id": 21, + }, + { + "has_default": false, + "mode": "in", + "name": "b", + "type_id": 21, + }, + ], + "argument_types": "a smallint, b smallint", + "behavior": "STABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION test_schema.test_func_renamed(a smallint, b smallint) + RETURNS integer + LANGUAGE sql + STABLE SECURITY DEFINER + SET role TO 'postgres' + SET search_path TO 'hooks', 'auth' + AS $function$select b - a$function$ + ", + "config_params": { + "role": "postgres", + "search_path": "hooks, auth", + }, + "definition": "select b - a", + "id": Any, + "identity_argument_types": "a smallint, b smallint", + "is_set_returning_function": false, + "language": "sql", + "name": "test_func_renamed", + "return_type": "integer", + "return_type_id": 23, + "return_type_relation_id": null, + "schema": "test_schema", + "security_definer": true, + }, + "error": null, + } + ` + ) + + // Remove function + const { sql: removeSql } = await pgMeta.functions.remove(resUpdated!) + await executeQuery(removeSql) + // Verify function is removed + const { sql: verifyRemoveSql } = await pgMeta.functions.retrieve({ id: functionId }) + const result = await executeQuery(verifyRemoveSql) + expect(result).toHaveLength(0) +}) + +withTestDatabase('retrieve set-returning function', async ({ executeQuery }) => { + // Retrieve function + const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.functions.retrieve({ + schema: 'public', + name: 'function_returning_set_of_rows', + args: [], + }) + const retrieve = await executeQuery(retrieveSql) + const res = retrieveZod.parse(retrieve[0]) + expect(res).toMatchInlineSnapshot( + { + id: expect.any(Number), + return_type_id: expect.any(Number), + return_type_relation_id: expect.any(Number), + }, + ` + { + "args": [], + "argument_types": "", + "behavior": "STABLE", + "complete_statement": "CREATE OR REPLACE FUNCTION public.function_returning_set_of_rows() + RETURNS SETOF users + LANGUAGE sql + STABLE + AS $function$ + select * from public.users; + $function$ + ", + "config_params": null, + "definition": " + select * from public.users; + ", + "id": Any, + "identity_argument_types": "", + "is_set_returning_function": true, + "language": "sql", + "name": "function_returning_set_of_rows", + "return_type": "SETOF users", + "return_type_id": Any, + "return_type_relation_id": Any, + "schema": "public", + "security_definer": false, + } + ` + ) +}) + +withTestDatabase('create function with various config_params values', async ({ executeQuery }) => { + // Set initial application_name for consistent testing + await executeQuery("SET application_name = 'current-app-name'") + + const { sql: createSql1 } = await pgMeta.functions.create({ + name: 'test_func_config_1', + schema: 'public', + definition: 'select 1', + return_type: 'integer', + language: 'sql', + config_params: { + search_path: '""', // Should become '' + application_name: 'FROM CURRENT', // Special syntax: SET param FROM CURRENT + work_mem: "'8MB'", // Regular syntax: SET param TO value + }, + }) + await executeQuery(createSql1) + + // Verify the function was created correctly + const { sql: retrieveSql1, zod: retrieveZod } = await pgMeta.functions.retrieve({ + name: 'test_func_config_1', + schema: 'public', + args: [], + }) + const result1 = retrieveZod.parse((await executeQuery(retrieveSql1))[0]) + + expect(result1).toBeDefined() + expect(result1!.config_params).toEqual({ + search_path: '""', + application_name: 'current-app-name', + work_mem: '8MB', + }) + + // Clean up + const { sql: removeSql1 } = await pgMeta.functions.remove(result1!) + await executeQuery(removeSql1) +}) diff --git a/packages/pg-meta/tsconfig.json b/packages/pg-meta/tsconfig.json index 97945ba9796..6970ef0a747 100644 --- a/packages/pg-meta/tsconfig.json +++ b/packages/pg-meta/tsconfig.json @@ -1,7 +1,8 @@ { - "extends": "tsconfig/base.json", + "extends": "../tsconfig/base.json", "compilerOptions": { - "module": "ESNext" + "module": "ESNext", + "moduleResolution": "bundler" }, "include": ["."], "exclude": ["dist", "build", "node_modules"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ddf279f674..ccbc6da9d6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -862,7 +862,7 @@ importers: version: 9.3.4 '@testing-library/jest-dom': specifier: ^6.4.6 - version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.37.0)) + version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4) '@testing-library/react': specifier: ^14.0.0 version: 14.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1390,7 +1390,7 @@ importers: dependencies: '@mertasan/tailwindcss-variables': specifier: ^2.2.3 - version: 2.7.0(autoprefixer@10.4.16(postcss@8.5.1))(postcss@8.5.1) + version: 2.7.0(autoprefixer@10.4.16(postcss@8.4.38))(postcss@8.4.38) '@radix-ui/colors': specifier: ^0.1.8 version: 0.1.9 @@ -1475,12 +1475,18 @@ importers: specifier: ^3.22.4 version: 3.23.8 devDependencies: + '@types/pg': + specifier: ^8.11.11 + version: 8.11.11 npm-run-all: specifier: ^4.1.5 version: 4.1.5 - postgres: - specifier: ^3.4.5 - version: 3.4.5 + pg: + specifier: ^8.13.1 + version: 8.13.1 + postgres-array: + specifier: ^3.0.2 + version: 3.0.2 typescript: specifier: ~5.5.0 version: 5.5.2 @@ -1637,7 +1643,7 @@ importers: version: 0.436.0(react@18.2.0) next: specifier: 'catalog:' - version: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) + version: 14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1737,7 +1743,7 @@ importers: version: 3.6.1 '@testing-library/jest-dom': specifier: ^6.1.3 - version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.37.0)) + version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4) '@testing-library/react': specifier: ^14.0.0 version: 14.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1836,7 +1842,7 @@ importers: version: 0.33.0 next: specifier: 'catalog:' - version: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) + version: 14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) next-themes: specifier: '*' version: 0.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1906,7 +1912,7 @@ importers: version: 10.1.0 '@testing-library/jest-dom': specifier: ^6.4.6 - version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.37.0)) + version: 6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4) '@testing-library/react': specifier: ^16.0.0 version: 16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1933,7 +1939,7 @@ importers: version: link:../api-types next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0) + version: 0.9.13(next@14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0) typescript: specifier: ~5.5.0 version: 5.5.2 @@ -5769,6 +5775,9 @@ packages: '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + '@types/pg@8.11.11': + resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==} + '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} @@ -7625,7 +7634,6 @@ packages: resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==} peerDependencies: eslint: '>= 5' - bundledDependencies: [] eslint-plugin-import@2.29.1: resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} @@ -10233,6 +10241,9 @@ packages: obliterator@2.0.4: resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -10438,6 +10449,9 @@ packages: pg-connection-string@2.6.2: resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + pg-connection-string@2.7.0: + resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + pg-format@1.0.4: resolution: {integrity: sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==} engines: {node: '>=4.0'} @@ -10450,21 +10464,29 @@ packages: resolution: {integrity: sha512-NoSsPqXxbkD8RIe+peQCqiea4QzXgosdTKY8p7PsbbGsh2F8TifDj/vJxfuR8qJwNYrijdSs7uf0tAe6WOyCsQ==} engines: {node: '>=12.0.0'} - pg-pool@3.6.1: - resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.7.1: + resolution: {integrity: sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==} peerDependencies: pg: '>=8.0' pg-protocol@1.6.0: resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} - pg-protocol@1.7.0: - resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + pg-protocol@1.7.1: + resolution: {integrity: sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} + pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + pg@8.11.3: resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} engines: {node: '>= 8.0.0'} @@ -10474,6 +10496,15 @@ packages: pg-native: optional: true + pg@8.13.1: + resolution: {integrity: sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} @@ -10642,18 +10673,29 @@ packages: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - postgres@3.4.5: - resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==} + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} engines: {node: '>=12'} + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -15033,12 +15075,6 @@ snapshots: lodash: 4.17.21 postcss: 8.4.38 - '@mertasan/tailwindcss-variables@2.7.0(autoprefixer@10.4.16(postcss@8.5.1))(postcss@8.5.1)': - dependencies: - autoprefixer: 10.4.16(postcss@8.5.1) - lodash: 4.17.21 - postcss: 8.5.1 - '@monaco-editor/loader@1.4.0(monaco-editor@0.33.0)': dependencies: monaco-editor: 0.33.0 @@ -17512,7 +17548,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4(@types/node@20.12.11)(@vitest/ui@3.0.4)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.37.0))': + '@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0(supports-color@8.1.1))(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.12.11)(supports-color@8.1.1))(vitest@3.0.4)': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.24.7 @@ -17949,10 +17985,16 @@ snapshots: dependencies: '@types/pg': 8.6.1 + '@types/pg@8.11.11': + dependencies: + '@types/node': 22.12.0 + pg-protocol: 1.7.1 + pg-types: 4.0.2 + '@types/pg@8.6.1': dependencies: '@types/node': 22.12.0 - pg-protocol: 1.7.0 + pg-protocol: 1.7.1 pg-types: 2.2.0 '@types/phoenix@1.6.4': {} @@ -18728,16 +18770,6 @@ snapshots: postcss: 8.4.38 postcss-value-parser: 4.2.0 - autoprefixer@10.4.16(postcss@8.5.1): - dependencies: - browserslist: 4.23.3 - caniuse-lite: 1.0.30001651 - fraction.js: 4.3.6 - normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.5.1 - postcss-value-parser: 4.2.0 - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -20123,7 +20155,7 @@ snapshots: debug: 4.3.7(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.57.0(supports-color@8.1.1) - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -20135,7 +20167,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: @@ -20162,7 +20194,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0(supports-color@8.1.1) eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -23673,6 +23705,11 @@ snapshots: next: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) react: 18.2.0 + next-router-mock@0.9.13(next@14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react@18.2.0): + dependencies: + next: 14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) + react: 18.2.0 + next-seo@6.5.0(next@14.2.21(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: next: 14.2.21(@babel/core@7.24.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0) @@ -23712,6 +23749,34 @@ snapshots: - '@babel/core' - babel-plugin-macros + next@14.2.21(@babel/core@7.26.7(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.72.0): + dependencies: + '@next/env': 14.2.21 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001695 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.26.7(supports-color@8.1.1))(react@18.2.0) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.21 + '@next/swc-darwin-x64': 14.2.21 + '@next/swc-linux-arm64-gnu': 14.2.21 + '@next/swc-linux-arm64-musl': 14.2.21 + '@next/swc-linux-x64-gnu': 14.2.21 + '@next/swc-linux-x64-musl': 14.2.21 + '@next/swc-win32-arm64-msvc': 14.2.21 + '@next/swc-win32-ia32-msvc': 14.2.21 + '@next/swc-win32-x64-msvc': 14.2.21 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.49.1 + sass: 1.72.0 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + nice-try@1.0.5: {} no-case@3.0.4: @@ -23952,6 +24017,8 @@ snapshots: obliterator@2.0.4: {} + obuf@1.1.2: {} + on-exit-leak-free@2.1.2: {} once@1.4.0: @@ -24182,19 +24249,27 @@ snapshots: pg-connection-string@2.6.2: {} + pg-connection-string@2.7.0: {} + pg-format@1.0.4: {} pg-int8@1.0.1: {} pg-minify@1.6.3: {} - pg-pool@3.6.1(pg@8.11.3): + pg-numeric@1.0.2: {} + + pg-pool@3.7.1(pg@8.11.3): dependencies: pg: 8.11.3 + pg-pool@3.7.1(pg@8.13.1): + dependencies: + pg: 8.13.1 + pg-protocol@1.6.0: {} - pg-protocol@1.7.0: {} + pg-protocol@1.7.1: {} pg-types@2.2.0: dependencies: @@ -24204,13 +24279,33 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 + pg-types@4.0.2: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + pg@8.11.3: dependencies: buffer-writer: 2.0.0 packet-reader: 1.0.0 - pg-connection-string: 2.6.2 - pg-pool: 3.6.1(pg@8.11.3) - pg-protocol: 1.6.0 + pg-connection-string: 2.7.0 + pg-pool: 3.7.1(pg@8.11.3) + pg-protocol: 1.7.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + pg@8.13.1: + dependencies: + pg-connection-string: 2.7.0 + pg-pool: 3.7.1(pg@8.13.1) + pg-protocol: 1.7.1 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -24404,13 +24499,21 @@ snapshots: postgres-bytea@1.0.0: {} + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + postgres-date@1.0.7: {} + postgres-date@2.1.0: {} + postgres-interval@1.2.0: dependencies: xtend: 4.0.2 - postgres@3.4.5: {} + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} prelude-ls@1.2.1: {} @@ -26000,6 +26103,13 @@ snapshots: optionalDependencies: '@babel/core': 7.24.7(supports-color@8.1.1) + styled-jsx@5.1.1(@babel/core@7.26.7(supports-color@8.1.1))(react@18.2.0): + dependencies: + client-only: 0.0.1 + react: 18.2.0 + optionalDependencies: + '@babel/core': 7.26.7(supports-color@8.1.1) + stylis@4.3.1: {} sucrase@3.34.0: From fc1b439665e420fefe724b426a00a04ec6f9d176 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Fri, 14 Feb 2025 07:07:36 -0330 Subject: [PATCH 04/18] what on earth (#33598) * what on earth * Maybe --- .../Storage/StorageExplorer/FileExplorer.tsx | 2 +- .../StorageExplorer/FileExplorerColumn.tsx | 215 +++++++++--------- 2 files changed, 110 insertions(+), 107 deletions(-) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx index fde79b07903..a99314b9b69 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx @@ -46,7 +46,7 @@ const FileExplorer = ({ return (
diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx index 45f095e15ed..3bb49eb2f85 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -142,134 +142,137 @@ const FileExplorerColumn = ({ ) return ( -
+
{ - const eventTarget = get(event.target, ['className'], '') - if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return - onSelectColumnEmptySpace(index) - }} - > - {/* Checkbox selection for select all */} - {view === STORAGE_VIEWS.COLUMNS && ( -
event.stopPropagation()} - > - {columnFiles.length > 0 ? ( - <> - -

Select all {columnFiles.length} files

- - ) : ( -

No files available for selection

- )} -
- )} + onContextMenu={displayMenu} + onDragOver={onDragOver} + onDrop={onDrop} + onClick={(event) => { + const eventTarget = get(event.target, ['className'], '') + if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return + onSelectColumnEmptySpace(index) + }} + > + {/* Checkbox selection for select all */} + {view === STORAGE_VIEWS.COLUMNS && ( +
event.stopPropagation()} + > + {columnFiles.length > 0 ? ( + <> + +

+ Select all {columnFiles.length} files +

+ + ) : ( +

No files available for selection

+ )} +
+ )} - {/* List Interface Header */} - {view === STORAGE_VIEWS.LIST && ( -
-
- -

Name

+ > +
+ +

Name

+
+

Size

+

Type

+

Created at

+

Last modified at

-

Size

-

Type

-

Created at

-

Last modified at

-
- )} + )} - {/* Shimmering loaders while fetching contents */} - {column.status === STORAGE_ROW_STATUS.LOADING && ( -
- - - -
- )} - - {/* Column Interface */} - (index !== 0 && index === columnItems.length ? 85 : 37)} - hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems} - isLoadingNextPage={column.isLoadingMoreItems} - onLoadNextPage={() => onColumnLoadMore(index, column)} - /> - - {/* Drag drop upload CTA for when column is empty */} - {!(snap.isSearching && itemSearchString.length > 0) && - column.items.length === 0 && - column.status !== STORAGE_ROW_STATUS.LOADING && ( -
- storage-placeholder -

Drop your files here

-

- Or upload them via the "Upload file" button above -

+ > + + +
)} - {snap.isSearching && - itemSearchString.length > 0 && - isEmpty && - column.status !== STORAGE_ROW_STATUS.LOADING && ( -
-

No results found in this folder

-

- Your search for "{itemSearchString}" did not return any results -

-
- )} + {/* Column Interface */} + (index !== 0 && index === columnItems.length ? 85 : 37)} + hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems} + isLoadingNextPage={column.isLoadingMoreItems} + onLoadNextPage={() => onColumnLoadMore(index, column)} + /> - {/* Drag drop upload CTA for when column has files */} - setIsDraggedOver(false)} - onDrop={() => setIsDraggedOver(false)} - /> + {/* Drag drop upload CTA for when column is empty */} + {!(snap.isSearching && itemSearchString.length > 0) && + column.items.length === 0 && + column.status !== STORAGE_ROW_STATUS.LOADING && ( +
+ storage-placeholder +

Drop your files here

+

+ Or upload them via the "Upload file" button above +

+
+ )} + {snap.isSearching && + itemSearchString.length > 0 && + isEmpty && + column.status !== STORAGE_ROW_STATUS.LOADING && ( +
+

No results found in this folder

+

+ Your search for "{itemSearchString}" did not return any results +

+
+ )} + + {/* Drag drop upload CTA for when column has files */} + setIsDraggedOver(false)} + onDrop={() => setIsDraggedOver(false)} + /> +
{/* List interface footer */} {view === STORAGE_VIEWS.LIST && (

@@ -277,7 +280,7 @@ const FileExplorerColumn = ({

)} -
+ ) } From 3521b9004ffac0e10b484391053bd9cb3419400a Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:11:05 +0100 Subject: [PATCH 05/18] clean up old logs feature flags (#33487) * clean up loki ff * rm loki ff * rm logsDetailV2 ff * rm cloneToNewProject ff --- .../Database/Backups/DatabaseBackupsNav.tsx | 4 +-- .../LogDrainDestinationSheetForm.tsx | 27 +++++++--------- .../interfaces/LogDrains/LogDrains.tsx | 26 +++++++-------- .../interfaces/Settings/Logs/LogSelection.tsx | 32 +------------------ 4 files changed, 25 insertions(+), 64 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx index 90deb1ac302..f13ffca7e40 100644 --- a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx +++ b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx @@ -1,5 +1,4 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { useFlag } from 'hooks/ui/useFlag' import Link from 'next/link' import React from 'react' @@ -10,7 +9,6 @@ type Props = { } function DatabaseBackupsNav({ active }: Props) { - const isCloneToNewProjectEnabled = useFlag('clonetonewproject') const { ref, cloud_provider } = useProjectContext()?.project || {} const navMenuItems = [ @@ -27,7 +25,7 @@ function DatabaseBackupsNav({ active }: Props) { href: `/project/${ref}/database/backups/pitr`, }, { - enabled: isCloneToNewProjectEnabled && cloud_provider !== 'FLY', + enabled: cloud_provider !== 'FLY', id: 'rtnp', label: (
diff --git a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx index eb49a069446..20af799952f 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx @@ -1,5 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { ExternalLink, TrashIcon } from 'lucide-react' +import { TrashIcon } from 'lucide-react' import { ReactNode, useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' @@ -7,7 +7,7 @@ import { z } from 'zod' import { useParams } from 'common' import { LogDrainData, useLogDrainsQuery } from 'data/log-drains/log-drains-query' -import { useFlag } from 'hooks/ui/useFlag' + import { Button, Form_Shadcn_, @@ -136,7 +136,6 @@ export function LogDrainDestinationSheetForm({ onSubmit: (values: z.infer) => void mode: 'create' | 'update' }) { - const lokiLogDrainsEnabled = useFlag('lokilogdrains') const CREATE_DEFAULT_HEADERS = { 'Content-Type': 'application/json', } @@ -281,18 +280,16 @@ export function LogDrainDestinationSheetForm({ {LOG_DRAIN_TYPES.find((t) => t.value === type)?.name} - {LOG_DRAIN_TYPES.map((type) => - type.value === 'loki' && !lokiLogDrainsEnabled ? null : ( - - {type.name} - - ) - )} + {LOG_DRAIN_TYPES.map((type) => ( + + {type.name} + + ))} diff --git a/apps/studio/components/interfaces/LogDrains/LogDrains.tsx b/apps/studio/components/interfaces/LogDrains/LogDrains.tsx index acd9d90ec13..f7009bbeaee 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrains.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrains.tsx @@ -27,7 +27,6 @@ import { import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { LOG_DRAIN_TYPES, LogDrainType } from './LogDrains.constants' -import { useFlag } from 'hooks/ui/useFlag' export function LogDrains({ onNewDrainClick, @@ -40,7 +39,6 @@ export function LogDrains({ const { isLoading: orgPlanLoading, plan } = useCurrentOrgPlan() const logDrainsEnabled = !orgPlanLoading && (plan?.id === 'team' || plan?.id === 'enterprise') - const lokiLogDrainsEnabled = useFlag('lokilogdrains') const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [selectedLogDrain, setSelectedLogDrain] = useState(null) @@ -93,19 +91,17 @@ export function LogDrains({ if (!isLoading && logDrains?.length === 0) { return (
- {LOG_DRAIN_TYPES.map((src) => - src.value === 'loki' && !lokiLogDrainsEnabled ? null : ( - { - onNewDrainClick(src.value) - }} - /> - ) - )} + {LOG_DRAIN_TYPES.map((src) => ( + { + onNewDrainClick(src.value) + }} + /> + ))}
) } diff --git a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx index 473123465e8..f6e1cb542ff 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx @@ -8,17 +8,9 @@ import { Tabs_Shadcn_, cn, } from 'ui' -import AuthSelectionRenderer from './LogSelectionRenderers/AuthSelectionRenderer' -import DatabaseApiSelectionRender from './LogSelectionRenderers/DatabaseApiSelectionRender' -import DatabasePostgresSelectionRender from './LogSelectionRenderers/DatabasePostgresSelectionRender' import DefaultPreviewSelectionRenderer from './LogSelectionRenderers/DefaultPreviewSelectionRenderer' -import FunctionInvocationSelectionRender from './LogSelectionRenderers/FunctionInvocationSelectionRender' -import FunctionLogsSelectionRender from './LogSelectionRenderers/FunctionLogsSelectionRender' import type { LogData, QueryType } from './Logs.types' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' -import { WarehouseSelectionRenderer } from './LogSelectionRenderers/WarehouseSelectionRenderer' -import { useFlag } from 'hooks/ui/useFlag' -import { IS_PLATFORM } from 'lib/constants' export interface LogSelectionProps { log?: LogData @@ -31,32 +23,10 @@ export interface LogSelectionProps { } const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectionProps) => { - const useNewLogDetail = useFlag('logsDetailV2') - const LogDetails = () => { if (error) return if (!log) return - if (useNewLogDetail || IS_PLATFORM === false) - return - - switch (queryType) { - case 'warehouse': - return - case 'api': - return - case 'database': - return - case 'pg_cron': - return - case 'fn_edge': - return - case 'functions': - return - case 'auth': - return - default: - return - } + return } return ( From ff8b1617a4e184e63df79c6ab02f2b6408fd12d9 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:07:01 -0500 Subject: [PATCH 06/18] fix: combine performance debugging sections into one page (#33609) --- apps/docs/content/guides/database/inspect.mdx | 180 +++++++++++++++++- .../content/guides/platform/performance.mdx | 165 +--------------- 2 files changed, 172 insertions(+), 173 deletions(-) diff --git a/apps/docs/content/guides/database/inspect.mdx b/apps/docs/content/guides/database/inspect.mdx index 7ac90292f7a..44d95c0bf20 100644 --- a/apps/docs/content/guides/database/inspect.mdx +++ b/apps/docs/content/guides/database/inspect.mdx @@ -1,9 +1,22 @@ --- -id: 'json' title: 'Debugging and monitoring' description: 'Inspecting your Postgres database for common issues around disk, query performance, index, locks, and more using the terminal.' --- +Database performance is a large topic and many factors can contribute. Some of the most common causes of poor performance include: + +- An inefficiently designed schema +- Inefficiently designed queries +- A lack of indexes causing slower than required queries over large tables +- Unused indexes causing slow `INSERT`, `UPDATE` and `DELETE` operations +- Not enough compute resources, such as memory, causing your database to go to disk for results too often +- Lock contention from multiple queries operating on highly utilized tables +- Large amount of bloat on your tables causing poor query planning + +You can examine your database and queries for these issues using either the [Supabase CLI](/docs/guides/local-development/cli/getting-started) or SQL. + +## Using the CLI + The Supabase CLI comes with a range of tools to help inspect your Postgres instances for potential issues. The CLI gets the information from Postgres internals. Therefore, most tools provided are compatible with any Postgres databases regardless if they are a Supabase project or not. You can find installation instructions for the the Supabase CLI here. @@ -47,7 +60,7 @@ supabase link --project-ref Then the CLI will automatically connect to your Supabase project whenever you are in the project folder and you no longer need to provide `—db-url`. -## Inspection commands +### Inspection commands Below are the `db` inspection commands provided, grouped by different use cases. @@ -57,7 +70,7 @@ Some commands might require `pg_stat_statements` to be enabled or a specific Pos -### Disk storage +#### Disk storage These commands are handy if you are running low on disk storage: @@ -68,7 +81,7 @@ These commands are handy if you are running low on disk storage: - [index-sizes](/docs/reference/cli/supabase-inspect-db-index-sizes) - shows the sizes of individual index - [table-index-sizes](/docs/reference/cli/supabase-inspect-db-table-index-sizes) - shows the sizes of indexes for each table -### Query performance +#### Query performance The commands below are useful if your Postgres database consumes a lot of resources like CPU, RAM or Disk IO. You can also use them to investigate slow queries. @@ -79,17 +92,17 @@ The commands below are useful if your Postgres database consumes a lot of resour - [long-running-queries](/docs/reference/cli/supabase-inspect-db-long-running-queries) - shows long running queries that are executing right now - [outliers](/docs/reference/cli/supabase-inspect-db-outliers) - shows queries with high execution time but low call count and queries with high proportion of execution time spent on synchronous I/O -### Locks +#### Locks - [locks](/docs/reference/cli/supabase-inspect-db-locks) - shows statements which have taken out an exclusive lock on a relation - [blocking](/docs/reference/cli/supabase-inspect-db-blocking) - shows statements that are waiting for locks to be released -### Connections +#### Connections - [role-connections](/docs/reference/cli/supabase-inspect-db-role-connections) - shows number of active connections for all database roles (Supabase-specific command) - [replication-slots](/docs/reference/cli/supabase-inspect-db-replication-slots) - shows information about replication slots on the database -## Notes on `pg_stat_statements` +### Notes on `pg_stat_statements` Following commands require `pg_stat_statements` to be enabled: calls, locks, cache-hit, blocking, unused-indexes, index-usage, bloat, outliers, table-record-counts, replication-slots, seq-scans, vacuum-stats, long-running-queries. @@ -97,6 +110,155 @@ When using `pg_stat_statements` also take note that it only stores the latest 5, Learn more about pg_stats [here](https://supabase.com/docs/guides/database/extensions/pg_stat_statements). -## Acknowledgements +## Using SQL -Supabase CLI's inspect commands are heavily inspired by the `pg-extras` tools. + + +If you're seeing an `insufficient privilege` error when viewing the Query Performance page from the dashboard, run this command: + +```shell +$ grant pg_read_all_stats to postgres; +``` + + + +### Postgres cumulative statistics system + +Postgres collects data about its own operations using the [cumulative statistics system](https://www.postgresql.org/docs/current/monitoring-stats.html). In addition to this, every Supabase project has the [pg_stat_statements extension](/docs/guides/database/extensions/pg_stat_statements) enabled by default. This extension records query execution performance details and is the best way to find inefficient queries. This information can be combined with the Postgres query plan analyzer to develop more efficient queries. + +Here are some example queries to get you started. + +### Most frequently called queries + +```sql +select + auth.rolname, + statements.query, + statements.calls, + -- -- Postgres 13, 14, 15 + statements.total_exec_time + statements.total_plan_time as total_time, + statements.min_exec_time + statements.min_plan_time as min_time, + statements.max_exec_time + statements.max_plan_time as max_time, + statements.mean_exec_time + statements.mean_plan_time as mean_time, + -- -- Postgres <= 12 + -- total_time, + -- min_time, + -- max_time, + -- mean_time, + statements.rows / statements.calls as avg_rows +from + pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid +order by statements.calls desc +limit 100; +``` + +This query shows: + +- query statistics, ordered by the number of times each query has been executed +- the role that ran the query +- the number of times it has been called +- the average number of rows returned +- the cumulative total time the query has spent running +- the min, max and mean query times. + +This provides useful information about the queries you run most frequently. Queries that have high `max_time` or `mean_time` times and are being called often can be good candidates for optimization. + +### Slowest queries by execution time + +```sql +select + auth.rolname, + statements.query, + statements.calls, + -- -- Postgres 13, 14, 15 + statements.total_exec_time + statements.total_plan_time as total_time, + statements.min_exec_time + statements.min_plan_time as min_time, + statements.max_exec_time + statements.max_plan_time as max_time, + statements.mean_exec_time + statements.mean_plan_time as mean_time, + -- -- Postgres <= 12 + -- total_time, + -- min_time, + -- max_time, + -- mean_time, + statements.rows / statements.calls as avg_rows +from + pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid +order by max_time desc +limit 100; +``` + +This query will show you statistics about queries ordered by the maximum execution time. It is similar to the query above ordered by calls, but this one highlights outliers that may have high executions times. Queries which have high or mean execution times are good candidates for optimization. + +### Most time consuming queries + +```sql +select + auth.rolname, + statements.query, + statements.calls, + statements.total_exec_time + statements.total_plan_time as total_time, + to_char( + ( + (statements.total_exec_time + statements.total_plan_time) / sum( + statements.total_exec_time + statements.total_plan_time + ) over () + ) * 100, + 'FM90D0' + ) || '%' as prop_total_time +from + pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid +order by total_time desc +limit 100; +``` + +This query will show you statistics about queries ordered by the cumulative total execution time. It shows the total time the query has spent running as well as the proportion of total execution time the query has taken up. + +Queries which are the most time consuming are not necessarily bad, you may have a very efficient and frequently ran queries that end up taking a large total % time, but it can be useful to help spot queries that are taking up more time than they should. + +### Hit rate + +Generally for most applications a small percentage of data is accessed more regularly than the rest. To make sure that your regularly accessed data is available, Postgres tracks your data access patterns and keeps this in its [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache. + +Applications with lower cache hit rates generally perform more poorly since they have to hit the disk to get results rather than serving them from memory. Very poor hit rates can also cause you to burst past your [Disk IO limits](./compute-add-ons#disk-io) causing significant performance issues. + +You can view your cache and index hit rate by executing the following query: + +```sql +select + 'index hit rate' as name, + (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) * 100 as ratio +from pg_statio_user_indexes +union all +select + 'table hit rate' as name, + sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100 as ratio +from pg_statio_user_tables; +``` + +This shows the ratio of data blocks fetched from the Postgres [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache against the data blocks that were read from disk/OS cache. + +If either of your index or table hit rate are < 99% then this can indicate your compute plan is too small for your current workload and you would benefit from more memory. [Upgrading your compute](./compute-add-ons) is easy and can be done from your [project dashboard](https://supabase.com/dashboard/project/_/settings/compute-and-disk). + +### Optimizing poor performing queries + +Postgres has built in tooling to help you optimize poorly performing queries. You can use the [query plan analyzer](https://www.postgresql.org/docs/current/sql-explain.html) on any expensive queries that you have identified: + +```sql +explain analyze ; +``` + +When you include `analyze` in the explain statement, the database attempts to execute the query and provides a detailed query plan along with actual execution times. So, be careful using `explain analyze` with `insert`/`update`/`delete` queries, because the query will actually run, and could have unintended side-effects. + +If you run just `explain` without the `analyze` keyword, the database will only perform query planning without actually executing the query. This approach can be beneficial when you want to inspect the query plan without affecting the database or if you encounter timeouts in your queries. + +Using the query plan analyzer to optimize your queries is a large topic, with a number of online resources available: + +- [Official docs.](https://www.postgresql.org/docs/current/using-explain.html) +- [The Art of PostgreSQL.](https://theartofpostgresql.com/explain-plan-visualizer/) +- [Postgres Wiki.](https://wiki.postgresql.org/wiki/Using_EXPLAIN) +- [Enterprise DB.](https://www.enterprisedb.com/blog/postgresql-query-optimization-performance-tuning-with-explain-analyze) + +You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../platform/metrics) to better understand the behavior of your DB and the queries you're executing against it. diff --git a/apps/docs/content/guides/platform/performance.mdx b/apps/docs/content/guides/platform/performance.mdx index 8b2f4a85164..46c2108da60 100644 --- a/apps/docs/content/guides/platform/performance.mdx +++ b/apps/docs/content/guides/platform/performance.mdx @@ -8,170 +8,7 @@ The Supabase platform automatically optimizes your Postgres database to take adv ## Examining query performance -Unoptimized queries are a major cause of poor database performance. The techniques on this page can help you identify and understand queries that take the most time and resources from your database. - -Database performance is a large topic and many factors can contribute. Some of the most common causes of poor performance include: - -- An inefficiently designed schema -- Inefficiently designed queries -- A lack of indexes causing slower than required queries over large tables -- Unused indexes causing slow `INSERT`, `UPDATE` and `DELETE` operations -- Not enough compute resources, such as memory, causing your database to go to disk for results too often -- Lock contention from multiple queries operating on highly utilized tables -- Large amount of bloat on your tables causing poor query planning - -Thankfully there are solutions to all these issues, which we will cover in the following sections. - - - -If you're seeing an `insufficient privilege` error when viewing the Query Performance page from the dashboard, run this command: - -```shell -$ grant pg_read_all_stats to postgres; -``` - - - -### Postgres cumulative statistics system - -Postgres collects data about its own operations using the [cumulative statistics system](https://www.postgresql.org/docs/current/monitoring-stats.html). In addition to this, every Supabase project has the [pg_stat_statements extension](/docs/guides/database/extensions/pg_stat_statements) enabled by default. This extension records query execution performance details and is the best way to find inefficient queries. This information can be combined with the Postgres query plan analyzer to develop more efficient queries. - -Here are some example queries to get you started. - -#### Most frequently called queries: - -```sql -select - auth.rolname, - statements.query, - statements.calls, - -- -- Postgres 13, 14, 15 - statements.total_exec_time + statements.total_plan_time as total_time, - statements.min_exec_time + statements.min_plan_time as min_time, - statements.max_exec_time + statements.max_plan_time as max_time, - statements.mean_exec_time + statements.mean_plan_time as mean_time, - -- -- Postgres <= 12 - -- total_time, - -- min_time, - -- max_time, - -- mean_time, - statements.rows / statements.calls as avg_rows -from - pg_stat_statements as statements - inner join pg_authid as auth on statements.userid = auth.oid -order by statements.calls desc -limit 100; -``` - -This query shows: - -- query statistics, ordered by the number of times each query has been executed -- the role that ran the query -- the number of times it has been called -- the average number of rows returned -- the cumulative total time the query has spent running -- the min, max and mean query times. - -This provides useful information about the queries you run most frequently. Queries that have high `max_time` or `mean_time` times and are being called often can be good candidates for optimization. - -#### Slowest queries by execution time: - -```sql -select - auth.rolname, - statements.query, - statements.calls, - -- -- Postgres 13, 14, 15 - statements.total_exec_time + statements.total_plan_time as total_time, - statements.min_exec_time + statements.min_plan_time as min_time, - statements.max_exec_time + statements.max_plan_time as max_time, - statements.mean_exec_time + statements.mean_plan_time as mean_time, - -- -- Postgres <= 12 - -- total_time, - -- min_time, - -- max_time, - -- mean_time, - statements.rows / statements.calls as avg_rows -from - pg_stat_statements as statements - inner join pg_authid as auth on statements.userid = auth.oid -order by max_time desc -limit 100; -``` - -This query will show you statistics about queries ordered by the maximum execution time. It is similar to the query above ordered by calls, but this one highlights outliers that may have high executions times. Queries which have high or mean execution times are good candidates for optimization. - -#### Most time consuming queries: - -```sql -select - auth.rolname, - statements.query, - statements.calls, - statements.total_exec_time + statements.total_plan_time as total_time, - to_char( - ( - (statements.total_exec_time + statements.total_plan_time) / sum( - statements.total_exec_time + statements.total_plan_time - ) over () - ) * 100, - 'FM90D0' - ) || '%' as prop_total_time -from - pg_stat_statements as statements - inner join pg_authid as auth on statements.userid = auth.oid -order by total_time desc -limit 100; -``` - -This query will show you statistics about queries ordered by the cumulative total execution time. It shows the total time the query has spent running as well as the proportion of total execution time the query has taken up. - -Queries which are the most time consuming are not necessarily bad, you may have a very efficient and frequently ran queries that end up taking a large total % time, but it can be useful to help spot queries that are taking up more time than they should. - -### Hit rate - -Generally for most applications a small percentage of data is accessed more regularly than the rest. To make sure that your regularly accessed data is available, Postgres tracks your data access patterns and keeps this in its [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache. - -Applications with lower cache hit rates generally perform more poorly since they have to hit the disk to get results rather than serving them from memory. Very poor hit rates can also cause you to burst past your [Disk IO limits](./compute-add-ons#disk-io) causing significant performance issues. - -You can view your cache and index hit rate by executing the following query: - -```sql -select - 'index hit rate' as name, - (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) * 100 as ratio -from pg_statio_user_indexes -union all -select - 'table hit rate' as name, - sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100 as ratio -from pg_statio_user_tables; -``` - -This shows the ratio of data blocks fetched from the Postgres [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache against the data blocks that were read from disk/OS cache. - -If either of your index or table hit rate are < 99% then this can indicate your compute plan is too small for your current workload and you would benefit from more memory. [Upgrading your compute](./compute-add-ons) is easy and can be done from your [project dashboard](https://supabase.com/dashboard/project/_/settings/compute-and-disk). - -### Optimizing poor performing queries - -Postgres has built in tooling to help you optimize poorly performing queries. You can use the [query plan analyzer](https://www.postgresql.org/docs/current/sql-explain.html) on any expensive queries that you have identified: - -```sql -explain analyze ; -``` - -When you include `analyze` in the explain statement, the database attempts to execute the query and provides a detailed query plan along with actual execution times. So, be careful using `explain analyze` with `insert`/`update`/`delete` queries, because the query will actually run, and could have unintended side-effects. - -If you run just `explain` without the `analyze` keyword, the database will only perform query planning without actually executing the query. This approach can be beneficial when you want to inspect the query plan without affecting the database or if you encounter timeouts in your queries. - -Using the query plan analyzer to optimize your queries is a large topic, with a number of online resources available: - -- [Official docs.](https://www.postgresql.org/docs/current/using-explain.html) -- [The Art of PostgreSQL.](https://theartofpostgresql.com/explain-plan-visualizer/) -- [Postgres Wiki.](https://wiki.postgresql.org/wiki/Using_EXPLAIN) -- [Enterprise DB.](https://www.enterprisedb.com/blog/postgresql-query-optimization-performance-tuning-with-explain-analyze) - -You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../platform/metrics) to better understand the behavior of your DB and the queries you're executing against it. +Unoptimized queries are a major cause of poor database performance. To analyze the performance of your queries, see the [Debugging and monitoring guide](/docs/guides/database/inspect). ## Optimizing the number of connections From 16eddf08d64fac1a350f0e7fc2dfabf53126770d Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:54:39 -0500 Subject: [PATCH 07/18] make rls admonition more prominent with stronger language (#33620) --- .../database/postgres/row-level-security.mdx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/docs/content/guides/database/postgres/row-level-security.mdx b/apps/docs/content/guides/database/postgres/row-level-security.mdx index 683fc7e1e5f..d2090a1ffb7 100644 --- a/apps/docs/content/guides/database/postgres/row-level-security.mdx +++ b/apps/docs/content/guides/database/postgres/row-level-security.mdx @@ -9,16 +9,25 @@ When you need granular authorization rules, nothing beats Postgres's [Row Level ## Row Level Security in Supabase + + +Supabase allows convenient and secure data access from the browser, as long as you enable RLS. + +RLS _must_ always be enabled on any tables stored in an exposed schema. By default, this is the `public` schema. + +RLS is enabled by default on tables created with the Table Editor in the dashboard. If you create one in raw SQL or with the SQL editor, remember to enable RLS yourself: + +```sql +alter table . +enable row level security; +``` + + + RLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be [combined with Supabase Auth](/docs/guides/database/postgres/row-level-security) for end-to-end user security from the browser to the database. RLS is a Postgres primitive and can provide "[defense in depth]()" to protect your data from malicious actors even when accessed through third-party tooling. - - -You should _always_ enable RLS on tables created in a public schema. This is done for you when you create a table with the Table Editor. If you create one in raw SQL or with the SQL Editor, remember to enable RLS yourself. - - - ## Policies [Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are Postgres's rule engine. Policies are easy to understand once you get the hang of them. Each policy is attached to a table, and the policy is executed every time a table is accessed. From ee728051f3b9b971b3b49cc57723cefbe40e6422 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:52:15 -0500 Subject: [PATCH 08/18] =?UTF-8?q?fix(design=20system):=20make=20suggested?= =?UTF-8?q?=20commands=20in=20assistant=20popover=20conf=E2=80=A6=20(#3362?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(design system): make suggested commands in assistant popover configurable I need to provide custom commands and right now they're hard-coded --- .../ui-patterns/AssistantChat/AssistantCommandsPopover.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ui-patterns/AssistantChat/AssistantCommandsPopover.tsx b/packages/ui-patterns/AssistantChat/AssistantCommandsPopover.tsx index 5b07624edac..ffe1996abe2 100644 --- a/packages/ui-patterns/AssistantChat/AssistantCommandsPopover.tsx +++ b/packages/ui-patterns/AssistantChat/AssistantCommandsPopover.tsx @@ -39,6 +39,8 @@ const AssistantCommandsPopover = forwardRef< suggestions?: string[] /* Whether an icon is being used in the AssistantChatForm */ usingIcon?: boolean + /* Available commands in suggestions popover */ + commands?: string[] } >( ( @@ -51,6 +53,7 @@ const AssistantCommandsPopover = forwardRef< setOpen, suggestions, usingIcon = false, + commands = ['fix', 'improve', 'explain', 'help'], ...props }, ref @@ -116,8 +119,6 @@ const AssistantCommandsPopover = forwardRef< const resultArray = value.split(/(\s+)/).filter(Boolean) - const commands = ['fix', 'improve', 'explain', 'help'] - return ( <> Date: Sat, 15 Feb 2025 13:46:28 +0100 Subject: [PATCH 09/18] update link to scheduled backups in dashboard backups.mdx (#31198) update the link to scheduled backups in the dashboard to database/backups/scheduled --- apps/docs/content/guides/platform/backups.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/backups.mdx b/apps/docs/content/guides/platform/backups.mdx index 1ab5f07ddbf..76583bef455 100644 --- a/apps/docs/content/guides/platform/backups.mdx +++ b/apps/docs/content/guides/platform/backups.mdx @@ -35,7 +35,7 @@ Once a project satisfies at least one of the requirements for physical backups t -You can confirm your project's backup type by navigating to [Database Backups > Scheduled backups](https://supabase.com/dashboard/project/_/backups/scheduled) and if you can download a backup then it is logical, otherwise it is physical. +You can confirm your project's backup type by navigating to [Database Backups > Scheduled backups](https://supabase.com/dashboard/project/_/database/backups/scheduled) and if you can download a backup then it is logical, otherwise it is physical. However, if your project has the Point-in-Time Recovery (PITR) add-on then the backups are physical and you can view them in [Database Backups > Point in time](https://supabase.com/dashboard/project/_/database/backups/pitr). From 4dcce558902cfb0dace473b8cc2f1647954f9693 Mon Sep 17 00:00:00 2001 From: Illia Basalaiev <44750366+Ellba@users.noreply.github.com> Date: Sat, 15 Feb 2025 17:21:50 +0100 Subject: [PATCH 10/18] add myself to humans.txt (#33631) Co-authored-by: Illia Basalaiev --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index aceecb2178c..02c9ac288e0 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -37,6 +37,7 @@ Greg Richardson Guilherme Souza Haydn Maley Hieu Pham +Illia Basalaiev Inian P Ivan Vasilov Jenny Kibiri From 8ef521bded138de17e66a0ebc72b6ab077c84665 Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Sun, 16 Feb 2025 15:14:46 +0200 Subject: [PATCH 11/18] docs: updating downloading logical backups guide (#33309) --- .../download-logical-backups.mdx | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/apps/docs/content/troubleshooting/download-logical-backups.mdx b/apps/docs/content/troubleshooting/download-logical-backups.mdx index bc1525d5558..d6c046a265f 100644 --- a/apps/docs/content/troubleshooting/download-logical-backups.mdx +++ b/apps/docs/content/troubleshooting/download-logical-backups.mdx @@ -6,25 +6,8 @@ github_url = "https://github.com/orgs/supabase/discussions/33045" database_id = "91ae4c49-bf22-4b8c-b96b-cd11f1c38158" --- -If you're unable to download backups due to physical backups being enabled, you can use the PostgreSQL utility `pg_dumpall` as an alternative. This tool offers more flexibility and control for creating backups. +If you're unable to download backups due to physical backups being enabled, you can use the Supabase CLI command pgdump as an alternative. -You can use the command below to backup your database: -`pg_dumpall -h your-db-host.supabase.co -U postgres > backup.sql` +For step-by-step instructions, check out our guide: [Backup & Restore](https://supabase.com/docs/guides/platform/migrating-within-supabase/backup-restore). -Or alternatively, you can create a backup of your Supabase database and store it in your repository by running the following script: https://github.com/mansueli/Supa-Backup - -To retrieve your database connection details from the dashboard: - -1. Go to your Dashboard. -2. Select your Project. -3. Navigate to: https://supabase.com/dashboard/project/_?showConnect=true. -4. Or click the "Connect" icon in the dashboard. -5. There, you’ll see the connection strings that we have available. - -image - -Additionally, please, refer to our documentation on [Database Backups](https://supabase.com/docs/guides/platform/backups). +You can also learn more about how backups work in Supabase here: [Database Backups](https://supabase.com/docs/guides/platform/backups). From 8b4fb55b54f4ce5c3249f1e048c4b56729453e0e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 17 Feb 2025 15:09:36 +0800 Subject: [PATCH 12/18] Add IS_PLATFORM checks for some unnecessary queries and UI for local (#33557) * Add IS_PLATFORM checks for some unnecessary queries and UI for local * Revert changes for analytics/endpoint/[name] * Smol * Smol --------- Co-authored-by: Terry Sutton --- .../interfaces/Settings/Logs/PreviewFilterPanel.tsx | 4 +++- apps/studio/data/analytics/warehouse-collections-query.ts | 3 ++- apps/studio/data/analytics/warehouse-tenant-query.ts | 5 +++-- apps/studio/data/read-replicas/load-balancers-query.ts | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx index 43123d2c2ea..53e0ff6f0e2 100644 --- a/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx @@ -8,6 +8,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import CSVButton from 'components/ui/CSVButton' import DatabaseSelector from 'components/ui/DatabaseSelector' import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query' +import { IS_PLATFORM } from 'lib/constants' import { Button, Input, Tooltip, TooltipContent, TooltipTrigger, cn } from 'ui' import DatePickers from './Logs.DatePickers' import { @@ -72,7 +73,8 @@ const PreviewFilterPanel = ({ const { data: loadBalancers } = useLoadBalancersQuery({ projectRef: ref }) // [Joshen] These are the routes tested that can show replica logs - const showDatabaseSelector = LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname) + const showDatabaseSelector = + IS_PLATFORM && LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname) const hasEdits = search !== defaultSearchValue diff --git a/apps/studio/data/analytics/warehouse-collections-query.ts b/apps/studio/data/analytics/warehouse-collections-query.ts index 75de23c686f..4fde686a646 100644 --- a/apps/studio/data/analytics/warehouse-collections-query.ts +++ b/apps/studio/data/analytics/warehouse-collections-query.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { get, handleError } from 'data/fetchers' +import { IS_PLATFORM } from 'lib/constants' import { analyticsKeys } from './keys' type WarehouseCollectionsVariables = { @@ -35,6 +36,6 @@ export const useWarehouseCollectionsQuery = ( analyticsKeys.warehouseCollections(projectRef), ({ signal }) => getWarehouseCollections({ projectRef }, signal), { - enabled: enabled && !!projectRef, + enabled: enabled && !!projectRef && IS_PLATFORM, } ) diff --git a/apps/studio/data/analytics/warehouse-tenant-query.ts b/apps/studio/data/analytics/warehouse-tenant-query.ts index 19c35c557a1..624a6719641 100644 --- a/apps/studio/data/analytics/warehouse-tenant-query.ts +++ b/apps/studio/data/analytics/warehouse-tenant-query.ts @@ -1,7 +1,8 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' -import { analyticsKeys } from './keys' import { get, handleError } from 'data/fetchers' +import { IS_PLATFORM } from 'lib/constants' import { ResponseError } from 'types' +import { analyticsKeys } from './keys' export type WarehouseTenantVariables = { projectRef: string @@ -41,7 +42,7 @@ export const useWarehouseTenantQuery = ( analyticsKeys.warehouseTenant(projectRef), ({ signal }) => getWarehouseTenant({ projectRef }, signal), { - enabled: enabled && !!projectRef, + enabled: enabled && !!projectRef && IS_PLATFORM, staleTime: Infinity, // 2H mins cache time cacheTime: 120 * 60 * 1000, diff --git a/apps/studio/data/read-replicas/load-balancers-query.ts b/apps/studio/data/read-replicas/load-balancers-query.ts index ceb0058d080..37d35418886 100644 --- a/apps/studio/data/read-replicas/load-balancers-query.ts +++ b/apps/studio/data/read-replicas/load-balancers-query.ts @@ -2,6 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import type { components } from 'data/api' import { get, handleError } from 'data/fetchers' +import { IS_PLATFORM } from 'lib/constants' import type { ResponseError } from 'types' import { replicaKeys } from './keys' @@ -37,7 +38,7 @@ export const useLoadBalancersQuery = ( replicaKeys.loadBalancers(projectRef), ({ signal }) => getLoadBalancers({ projectRef }, signal), { - enabled: enabled && typeof projectRef !== 'undefined', + enabled: enabled && typeof projectRef !== 'undefined' && IS_PLATFORM, ...options, } ) From 98a5f0607063d2f64a336b34caa0b665cdedad8e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 17 Feb 2025 15:23:38 +0800 Subject: [PATCH 13/18] Chore/next round of refactoring rqs to use data fetchers (#33556) * Update several files to use data fetcher methods * Remove unused files from lib/common/fetch * Revert "Remove unused files from lib/common/fetch" This reverts commit b314b05f065c3ed91ee586a1e8a8b4c3254d4f80. * Fix GraphQL * Fix types * Add IS_PLATFORM check in useOrgUsageQuery * Fix type issues --- .../Integrations/GraphQL/GraphiQLTab.tsx | 3 +- .../OAuthApps/AuthorizedAppRow.tsx | 5 +- .../Organization/OAuthApps/OAuthAppRow.tsx | 15 ++++-- .../Organization/OAuthApps/OAuthApps.tsx | 4 +- .../OAuthApps/PublishAppSidePanel/index.tsx | 6 +-- .../integrations-query-org-only.ts | 25 ++++----- ...el-installed-connection-delete-mutation.ts | 26 ++++----- .../data/integrations/integrations.types.ts | 5 -- .../vercel-integration-create-mutation.ts | 28 +++++----- .../oauth/authorized-app-revoke-mutation.ts | 12 +++-- .../data/oauth/authorized-apps-query.ts | 15 +++--- .../data/oauth/oauth-app-create-mutation.ts | 30 +++++------ .../data/oauth/oauth-app-delete-mutation.ts | 12 +++-- apps/studio/data/oauth/oauth-apps-query.ts | 27 ++++------ .../projects/project-transfer-mutation.ts | 22 ++++---- .../project-transfer-preview-query.ts | 54 ++++--------------- apps/studio/data/usage/org-usage-query.ts | 3 +- 17 files changed, 123 insertions(+), 169 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx b/apps/studio/components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx index bf7be14c44a..b4e3ae6ecb1 100644 --- a/apps/studio/components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx +++ b/apps/studio/components/interfaces/Integrations/GraphQL/GraphiQLTab.tsx @@ -41,7 +41,8 @@ export const GraphiQLTab = () => { const fetcher = useMemo(() => { const fetcherFn = createGraphiQLFetcher({ - url: `${API_URL}/projects/${projectRef}/api/graphql`, + // [Joshen] Opting to hard code /platform for local to match the routes, so that it's clear what's happening + url: `${API_URL}${IS_PLATFORM ? '' : '/platform'}/projects/${projectRef}/api/graphql`, fetch, }) const customFetcher: Fetcher = async (graphqlParams, opts) => { diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx index 6d05d482ea5..b20002666fd 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/AuthorizedAppRow.tsx @@ -1,7 +1,8 @@ -import Table from 'components/to-be-cleaned/Table' -import type { AuthorizedApp } from 'data/oauth/authorized-apps-query' import dayjs from 'dayjs' import { Trash } from 'lucide-react' + +import Table from 'components/to-be-cleaned/Table' +import type { AuthorizedApp } from 'data/oauth/authorized-apps-query' import { Button } from 'ui' export interface AuthorizedAppRowProps { diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx index 5e15b6ee800..92d130cd256 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx @@ -7,6 +7,7 @@ import Table from 'components/to-be-cleaned/Table' import type { OAuthApp } from 'data/oauth/oauth-apps-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { copyToClipboard } from 'lib/helpers' +import { toast } from 'sonner' import { Button, DropdownMenu, @@ -56,11 +57,15 @@ const OAuthAppRow = ({ app, onSelectEdit, onSelectDelete }: OAuthAppRowProps) => icon={isCopied ? : } className="ml-2 px-1" onClick={() => { - copyToClipboard(app.client_id) - setIsCopied(true) - setTimeout(() => { - setIsCopied(false) - }, 3000) + if (app.client_id) { + copyToClipboard(app.client_id) + setIsCopied(true) + setTimeout(() => { + setIsCopied(false) + }, 3000) + } else { + toast('Unable to retrieve app client ID') + } }} />
diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx index a768502df9f..ae54fe1a1c7 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx @@ -1,4 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { X } from 'lucide-react' import { useState } from 'react' import { useParams } from 'common' @@ -18,7 +19,6 @@ import DeleteAppModal from './DeleteAppModal' import OAuthAppRow from './OAuthAppRow' import PublishAppModal from './PublishAppSidePanel' import RevokeAppModal from './RevokeAppModal' -import { X } from 'lucide-react' // [Joshen] Note on nav UX // Kang Ming mentioned that it might be better to split Published Apps and Authorized Apps into 2 separate tabs @@ -45,7 +45,7 @@ const OAuthApps = () => { } = useOAuthAppsQuery({ slug }, { enabled: canReadOAuthApps }) const sortedPublishedApps = publishedApps?.sort((a, b) => { - return Number(new Date(a.created_at)) - Number(new Date(b.created_at)) + return Number(new Date(a.created_at ?? '')) - Number(new Date(b.created_at ?? '')) }) const { diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/PublishAppSidePanel/index.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/PublishAppSidePanel/index.tsx index c14a7428e5d..560ad3e127d 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/PublishAppSidePanel/index.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/PublishAppSidePanel/index.tsx @@ -85,11 +85,11 @@ const PublishAppSidePanel = ({ if (selectedApp !== undefined) { setUrls( - selectedApp.redirect_uris.map((url) => { + selectedApp.redirect_uris?.map((url) => { return { id: uuidv4(), value: url } - }) + }) ?? [] ) - setScopes(selectedApp.scopes) + setScopes((selectedApp?.scopes ?? []) as OAuthScope[]) setIconUrl(selectedApp.icon === null ? undefined : selectedApp.icon) } else { setUrls([{ id: uuidv4(), value: '' }]) diff --git a/apps/studio/data/integrations/integrations-query-org-only.ts b/apps/studio/data/integrations/integrations-query-org-only.ts index 93872903086..b000749ccfa 100644 --- a/apps/studio/data/integrations/integrations-query-org-only.ts +++ b/apps/studio/data/integrations/integrations-query-org-only.ts @@ -1,25 +1,22 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' -import { get } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' -import type { Integration, IntegrationsVariables } from './integrations.types' +import { Integration } from './integrations.types' import { integrationKeys } from './keys' -export type IntegrationsResponse = Integration[] +type IntegrationsVariables = { + orgSlug?: string +} export async function getIntegrations({ orgSlug }: IntegrationsVariables, signal?: AbortSignal) { - if (!orgSlug) { - throw new Error('orgSlug is required') - } - const response = await get(`${API_URL}/integrations/${orgSlug}?expand=true`, { - signal, - }) - if (response.error) { - throw response.error - } + if (!orgSlug) throw new Error('orgSlug is required') - return response as IntegrationsResponse + const { data, error } = await get('/platform/integrations/{slug}', { + params: { path: { slug: orgSlug } }, + }) + if (error) handleError(error) + return data as unknown as Integration[] } export type IntegrationsData = Awaited> diff --git a/apps/studio/data/integrations/integrations-vercel-installed-connection-delete-mutation.ts b/apps/studio/data/integrations/integrations-vercel-installed-connection-delete-mutation.ts index 2f98285d5ad..13c93f60cce 100644 --- a/apps/studio/data/integrations/integrations-vercel-installed-connection-delete-mutation.ts +++ b/apps/studio/data/integrations/integrations-vercel-installed-connection-delete-mutation.ts @@ -1,8 +1,8 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' -import { delete_, isResponseOk } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' import { toast } from 'sonner' -import type { ResponseError, UserContent } from 'types' + +import { del, handleError } from 'data/fetchers' +import type { ResponseError } from 'types' import { integrationKeys } from './keys' type DeleteVariables = { @@ -15,21 +15,15 @@ export async function deleteConnection( { organization_integration_id, id }: DeleteVariables, signal?: AbortSignal ) { - if (!organization_integration_id) { - throw new Error('organization_integration_id is required') - } + if (!organization_integration_id) throw new Error('organization_integration_id is required') - const response = await delete_( - `${API_URL}/integrations/vercel/connections/${id}`, - { organization_integration_id }, - { signal } - ) + const { data, error } = await del('/platform/integrations/vercel/connections/{connection_id}', { + params: { path: { connection_id: id } }, + signal, + }) - if (!isResponseOk(response)) { - throw response.error - } - - return response + if (error) handleError(error) + return data } type DeleteContentData = Awaited> diff --git a/apps/studio/data/integrations/integrations.types.ts b/apps/studio/data/integrations/integrations.types.ts index f79843b59e5..cc5c7f3ccce 100644 --- a/apps/studio/data/integrations/integrations.types.ts +++ b/apps/studio/data/integrations/integrations.types.ts @@ -160,10 +160,6 @@ export type IntegrationProjectConnection = { metadata: Imetadata } -export type IntegrationsVariables = { - orgSlug?: string -} - export type IntegrationProjectConnectionPayload = { foreignProjectId: string supabaseProjectId: string @@ -177,7 +173,6 @@ export type userDetails = { primary_email: string } type addedBy = userDetails -type updatedBy = userDetails export type IntegrationName = 'Vercel' | 'GitHub' // | 'Netlify' export type VercelAccountType = 'Team' | 'Personal' diff --git a/apps/studio/data/integrations/vercel-integration-create-mutation.ts b/apps/studio/data/integrations/vercel-integration-create-mutation.ts index 3d8b5c19c3e..f3d1315c29a 100644 --- a/apps/studio/data/integrations/vercel-integration-create-mutation.ts +++ b/apps/studio/data/integrations/vercel-integration-create-mutation.ts @@ -1,7 +1,7 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' -import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' import { toast } from 'sonner' + +import { handleError, post } from 'data/fetchers' import type { ResponseError } from 'types' import { integrationKeys } from './keys' @@ -24,19 +24,21 @@ export async function createVercelIntegration({ source, teamId, }: VercelIntegrationCreateVariables) { - const response = await post(`${API_URL}/integrations/vercel`, { - code, - configuration_id: configurationId, - organization_slug: orgSlug, - metadata, - source, - teamId, + const { data, error } = await post('/platform/integrations/vercel', { + body: { + code, + configuration_id: configurationId, + organization_slug: orgSlug, + metadata: metadata as Record, + source, + teamId, + }, }) - if (response.error) { - throw response.error - } - return response + if (error) handleError(error) + // [Joshen] API isn't typed on this endpoint + // https://github.com/supabase/infrastructure/blob/develop/api/src/routes/platform/integrations/vercel/vercel-integration.controller.ts#L50 + return data as { id: string } } type VercelIntegrationCreateData = Awaited> diff --git a/apps/studio/data/oauth/authorized-app-revoke-mutation.ts b/apps/studio/data/oauth/authorized-app-revoke-mutation.ts index 105f164afec..9077e1f550f 100644 --- a/apps/studio/data/oauth/authorized-app-revoke-mutation.ts +++ b/apps/studio/data/oauth/authorized-app-revoke-mutation.ts @@ -1,8 +1,7 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { handleError, post } from 'data/fetchers' import type { ResponseError } from 'types' import { oauthAppKeys } from './keys' @@ -15,9 +14,12 @@ export async function revokeAuthorizedApp({ id, slug }: AuthorizedAppRevokeVaria if (!id) throw new Error('App ID is required') if (!slug) throw new Error('Organization slug is required') - const response = await post(`${API_URL}/organizations/${slug}/oauth/apps/${id}/revoke`, {}) - if (response.error) throw response.error - return response + const { data, error } = await post('/platform/organizations/{slug}/oauth/apps/{id}/revoke', { + params: { path: { slug, id } }, + }) + + if (error) handleError(error) + return data } type AuthorizedAppRevokeData = Awaited> diff --git a/apps/studio/data/oauth/authorized-apps-query.ts b/apps/studio/data/oauth/authorized-apps-query.ts index fcf8f11cf0d..a988e9cf04b 100644 --- a/apps/studio/data/oauth/authorized-apps-query.ts +++ b/apps/studio/data/oauth/authorized-apps-query.ts @@ -1,8 +1,8 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' -import { get } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' -import { oauthAppKeys } from './keys' + +import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' +import { oauthAppKeys } from './keys' export type AuthorizedAppsVariables = { slug?: string @@ -19,11 +19,12 @@ export type AuthorizedApp = { export async function getAuthorizedApps({ slug }: AuthorizedAppsVariables, signal?: AbortSignal) { if (!slug) throw new Error('Organization slug is required') - const response = await get(`${API_URL}/organizations/${slug}/oauth/apps?type=authorized`, { - signal, + const { data, error } = await get('/platform/organizations/{slug}/oauth/apps', { + params: { path: { slug }, query: { type: 'authorized' } }, }) - if (response.error) throw response.error - return response as AuthorizedApp[] + + if (error) handleError(error) + return data as AuthorizedApp[] } export type AuthorizedAppsData = Awaited> diff --git a/apps/studio/data/oauth/oauth-app-create-mutation.ts b/apps/studio/data/oauth/oauth-app-create-mutation.ts index ee07dbfa183..6092b3f340d 100644 --- a/apps/studio/data/oauth/oauth-app-create-mutation.ts +++ b/apps/studio/data/oauth/oauth-app-create-mutation.ts @@ -2,8 +2,8 @@ import type { OAuthScope } from '@supabase/shared-types/out/constants' import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { components } from 'api-types' +import { handleError, post } from 'data/fetchers' import type { ResponseError } from 'types' import { oauthAppKeys } from './keys' @@ -16,11 +16,7 @@ export type OAuthAppCreateVariables = { redirect_uris: string[] } -export type OAuthAppCreateResponse = { - id: string - client_id: string - client_secret: string -} +export type OAuthAppCreateResponse = components['schemas']['CreateOAuthAppResponse'] export async function createOAuthApp({ slug, @@ -30,15 +26,19 @@ export async function createOAuthApp({ scopes, redirect_uris, }: OAuthAppCreateVariables) { - const response = await post(`${API_URL}/organizations/${slug}/oauth/apps`, { - name, - website, - icon, - scopes, - redirect_uris, + const { data, error } = await post('/platform/organizations/{slug}/oauth/apps', { + params: { path: { slug } }, + body: { + name, + website, + icon, + scopes, + redirect_uris, + }, }) - if (response.error) throw response.error - return response as OAuthAppCreateResponse + + if (error) handleError(error) + return data } type OAuthAppCreateData = Awaited> diff --git a/apps/studio/data/oauth/oauth-app-delete-mutation.ts b/apps/studio/data/oauth/oauth-app-delete-mutation.ts index e0663b14984..4bf0ca78e47 100644 --- a/apps/studio/data/oauth/oauth-app-delete-mutation.ts +++ b/apps/studio/data/oauth/oauth-app-delete-mutation.ts @@ -1,8 +1,7 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { delete_ } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { del, handleError } from 'data/fetchers' import type { ResponseError } from 'types' import { oauthAppKeys } from './keys' @@ -15,9 +14,12 @@ export async function deleteOAuthApp({ id, slug }: OAuthAppDeleteVariables) { if (!id) throw new Error('OAuth app ID is required') if (!slug) throw new Error('Organization slug is required') - const response = await delete_(`${API_URL}/organizations/${slug}/oauth/apps/${id}?type=published`) - if (response.error) throw response.error - return response + const { data, error } = await del('/platform/organizations/{slug}/oauth/apps/{id}', { + params: { path: { slug, id } }, + }) + + if (error) handleError(error) + return data } type OAuthAppDeleteData = Awaited> diff --git a/apps/studio/data/oauth/oauth-apps-query.ts b/apps/studio/data/oauth/oauth-apps-query.ts index 63ea17efcd4..e8acb2ea9c5 100644 --- a/apps/studio/data/oauth/oauth-apps-query.ts +++ b/apps/studio/data/oauth/oauth-apps-query.ts @@ -1,7 +1,7 @@ -import type { OAuthScope } from '@supabase/shared-types/out/constants' import { useQuery, UseQueryOptions } from '@tanstack/react-query' -import { get } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' + +import { components } from 'api-types' +import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' import { oauthAppKeys } from './keys' @@ -9,26 +9,17 @@ export type OAuthAppsVariables = { slug?: string } -export type OAuthApp = { - id: string - icon: string | null - client_id: string - client_secret_alias: string - created_at: string - name: string - website: string - scopes: OAuthScope[] - redirect_uris: string[] -} +export type OAuthApp = components['schemas']['OAuthAppResponse'] export async function getOAuthApps({ slug }: OAuthAppsVariables, signal?: AbortSignal) { if (!slug) throw new Error('Organization slug is required') - const response = await get(`${API_URL}/organizations/${slug}/oauth/apps?type=published`, { - signal, + const { data, error } = await get('/platform/organizations/{slug}/oauth/apps', { + params: { path: { slug }, query: { type: 'published' } }, }) - if (response.error) throw response.error - return response as OAuthApp[] + + if (error) handleError(error) + return data } export type OAuthAppsData = Awaited> diff --git a/apps/studio/data/projects/project-transfer-mutation.ts b/apps/studio/data/projects/project-transfer-mutation.ts index 2a53c8ed3f3..8d9056c26db 100644 --- a/apps/studio/data/projects/project-transfer-mutation.ts +++ b/apps/studio/data/projects/project-transfer-mutation.ts @@ -1,8 +1,8 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { handleError, post } from 'data/fetchers' +import { ResponseError } from 'types' import { projectKeys } from './keys' export type ProjectTransferVariables = { @@ -10,10 +10,6 @@ export type ProjectTransferVariables = { targetOrganizationSlug?: string } -type ProjectTransferError = { - message: string -} - export async function transferProject({ projectRef, targetOrganizationSlug, @@ -25,10 +21,12 @@ export async function transferProject({ target_organization_slug: targetOrganizationSlug, } - const response = await post(`${API_URL}/projects/${projectRef}/transfer`, payload) - if (response.error) throw response.error - - return response + const { data, error } = await post('/platform/projects/{ref}/transfer', { + params: { path: { ref: projectRef } }, + body: payload, + }) + if (error) handleError(error) + return data } type ProjectTransferData = Awaited> @@ -38,12 +36,12 @@ export const useProjectTransferMutation = ({ onError, ...options }: Omit< - UseMutationOptions, + UseMutationOptions, 'mutationFn' > = {}) => { const queryClient = useQueryClient() - return useMutation( + return useMutation( (vars) => transferProject(vars), { async onSuccess(data, variables, context) { diff --git a/apps/studio/data/projects/project-transfer-preview-query.ts b/apps/studio/data/projects/project-transfer-preview-query.ts index 08bab5b916b..8e4f301b36b 100644 --- a/apps/studio/data/projects/project-transfer-preview-query.ts +++ b/apps/studio/data/projects/project-transfer-preview-query.ts @@ -1,6 +1,6 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' -import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' + +import { handleError, post } from 'data/fetchers' import { projectKeys } from './keys' export type ProjectTransferPreviewVariables = { @@ -10,40 +10,6 @@ export type ProjectTransferPreviewVariables = { export type PlanId = 'free' | 'pro' | 'team' | 'enterprise' -type MemberExceedingFreeProjectLimit = { - name: string - limit: number -} - -type PreviewTransferInfo = { - key: string - message: string -} - -export type PreviewProjectTransferResponse = { - valid: boolean - - warnings: PreviewTransferInfo[] - errors: PreviewTransferInfo[] - - members_exceeding_free_project_limit: MemberExceedingFreeProjectLimit[] - - has_permissions_on_source_organization: boolean - has_access_to_target_organization: boolean - - source_project_eligible: boolean - - target_organization_eligible: boolean | null - target_organization_has_free_project_slots: boolean | null - - credits_on_source_organization: number - costs_on_target_organization: number - charge_on_target_organization: number - - source_subscription_plan: PlanId - target_subscription_plan: PlanId | null -} - export async function previewProjectTransfer( { projectRef, targetOrganizationSlug }: ProjectTransferPreviewVariables, signal?: AbortSignal @@ -51,16 +17,14 @@ export async function previewProjectTransfer( if (!projectRef) throw new Error('projectRef is required') if (!targetOrganizationSlug) throw new Error('targetOrganizationSlug is required') - const response = await post( - `${API_URL}/projects/${projectRef}/transfer/preview`, - { - target_organization_slug: targetOrganizationSlug, - }, - { signal } - ) - if (response.error) throw response.error + const { data, error } = await post('/platform/projects/{ref}/transfer/preview', { + params: { path: { ref: projectRef } }, + body: { target_organization_slug: targetOrganizationSlug }, + signal, + }) - return response as PreviewProjectTransferResponse + if (error) handleError(error) + return data } export type ProjectTransferPreviewData = Awaited> diff --git a/apps/studio/data/usage/org-usage-query.ts b/apps/studio/data/usage/org-usage-query.ts index 07355b037b5..79124b869b6 100644 --- a/apps/studio/data/usage/org-usage-query.ts +++ b/apps/studio/data/usage/org-usage-query.ts @@ -2,6 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import type { components } from 'data/api' import { get, handleError } from 'data/fetchers' +import { IS_PLATFORM } from 'lib/constants' import type { ResponseError } from 'types' import { usageKeys } from './keys' @@ -42,7 +43,7 @@ export const useOrgUsageQuery = ( usageKeys.orgUsage(orgSlug, projectRef, start?.toISOString(), end?.toISOString()), ({ signal }) => getOrgUsage({ orgSlug, projectRef, start, end }, signal), { - enabled: enabled && typeof orgSlug !== 'undefined', + enabled: enabled && IS_PLATFORM && typeof orgSlug !== 'undefined', staleTime: 1000 * 60 * 30, // 30 mins, underlying usage data only refreshes once an hour, so safe to cache for a while ...options, } From fe47ff17f9ca0533d525b716c8d7777b21e6562d Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Mon, 17 Feb 2025 08:53:43 +0100 Subject: [PATCH 14/18] clean up logs status component FE-1403 (#33525) * clean up status component * rm type casts * fix type err * fix type err --- .../DatabaseApiColumnRender.tsx | 6 +- .../FunctionsEdgeColumnRender.tsx | 6 +- .../Settings/Logs/LogsFormatters.tsx | 72 ++++++++----------- 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Logs/LogColumnRenderers/DatabaseApiColumnRender.tsx b/apps/studio/components/interfaces/Settings/Logs/LogColumnRenderers/DatabaseApiColumnRender.tsx index 8eac7a4f6bf..1af90fe8c9d 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogColumnRenderers/DatabaseApiColumnRender.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogColumnRenderers/DatabaseApiColumnRender.tsx @@ -15,9 +15,9 @@ const columns: Column[] = [ return ( - - - + + + ) }, diff --git a/apps/studio/components/interfaces/Settings/Logs/LogColumnRenderers/FunctionsEdgeColumnRender.tsx b/apps/studio/components/interfaces/Settings/Logs/LogColumnRenderers/FunctionsEdgeColumnRender.tsx index 9d76d5a880f..3e2ea9b11d3 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogColumnRenderers/FunctionsEdgeColumnRender.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogColumnRenderers/FunctionsEdgeColumnRender.tsx @@ -15,9 +15,9 @@ const columns: Column[] = [ return ( - - - + + + ) }, diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx index f5b3c2d7b74..d7a25872953 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx @@ -9,6 +9,7 @@ import React from 'react' import { isUnixMicro, unixMicroToIsoTimestamp } from './Logs.utils' import { AlertCircle, Info } from 'lucide-react' import dayjs from 'dayjs' +import { cn } from 'ui' export const RowLayout: React.FC = ({ children }) => (
{children}
@@ -57,7 +58,7 @@ export const TextFormatter: React.FC<{ className?: string; value: string }> = ({ className, }) => {value} -export const ResponseCodeFormatter = ({ value }: any) => { +export const ResponseCodeFormatter = ({ value }: { value: string }) => { if (!value) { return (
@@ -66,66 +67,49 @@ export const ResponseCodeFormatter = ({ value }: any) => { ) } + const ResponseCodeItem = ({ + children, + className, + }: { + children: React.ReactNode + className?: string + }) => ( +
+
+ +
+
+ ) + const split = value.toString().split('')[0] switch (split) { // 2XX || 1XX responses case '1': + return {value} case '2': - return ( -
-
- -
-
- ) - break + return {value} // 5XX responses case '5': - return ( -
-
{value} - " - > - -
-
- ) - break // 4XX || 3XX responses case '4': case '3': - return ( -
-
{value} - " - > - -
-
- ) - break // All other responses default: return ( -
-
- -
-
+ + {value} + ) - break } } From 163604bccd50fe4562ef2168326c73dbe2d06e21 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Mon, 17 Feb 2025 04:33:04 -0330 Subject: [PATCH 15/18] Fix height of storage panel (#33624) --- apps/studio/components/ui/InfiniteList.tsx | 55 +++++++++++----------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/apps/studio/components/ui/InfiniteList.tsx b/apps/studio/components/ui/InfiniteList.tsx index 2a3ece04e04..8ed657cb897 100644 --- a/apps/studio/components/ui/InfiniteList.tsx +++ b/apps/studio/components/ui/InfiniteList.tsx @@ -111,33 +111,34 @@ function InfiniteList({ return ( <> - - {({ height, width }: { height: number; width: number }) => ( - - {({ onItemsRendered, ref }) => ( - { - ref(refy) - listRef.current = refy - }} - height={height ?? 0} - width={width ?? 0} - itemCount={itemCount} - itemData={itemData} - itemSize={getItemSize} - onItemsRendered={onItemsRendered} - > - {Item} - - )} - - )} - - +
+ + {({ height, width }: { height: number; width: number }) => ( + + {({ onItemsRendered, ref }) => ( + { + ref(refy) + listRef.current = refy + }} + height={height ?? 0} + width={width ?? 0} + itemCount={itemCount} + itemData={itemData} + itemSize={getItemSize} + onItemsRendered={onItemsRendered} + > + {Item} + + )} + + )} + +
Date: Mon, 17 Feb 2025 09:28:00 -0330 Subject: [PATCH 16/18] Chore/diff cmd (#33637) * Fix diff cmd * Cmd to run migration * Fix stephike markup --- .../declarative-database-schemas.mdx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx index 487cec57868..fd4b4010212 100644 --- a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx +++ b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx @@ -136,7 +136,7 @@ Some entities like views and enums expect columns to be declared in a specific o ```bash Terminal -supabase diff -f add_age +supabase db diff -f add_age ``` @@ -162,6 +162,23 @@ alter table "public"."employees" add column "age" smallint not null; + + + + Start the database locally and apply the pending migration. + + + + +```bash Terminal +supabase start && supabase migration up +``` + + + + + + ### Managing dependencies As your database schema evolves, you will probably start using more advanced entities like views and functions. These entities are notoriously verbose to manage using plain migrations because the entire body must be recreated whenever there is a change. Using declarative schema, you can now edit them in-place so it’s much easier to review. From b7df65f71fcd260b891a99c2d0bfbd33b5706d22 Mon Sep 17 00:00:00 2001 From: hallidayo <22655069+Hallidayo@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:33:08 +0000 Subject: [PATCH 17/18] www: supabase cron wrong url (#33368) url change --- apps/www/data/products/modules/cron.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/data/products/modules/cron.tsx b/apps/www/data/products/modules/cron.tsx index d8262777de4..17bcd2453f9 100644 --- a/apps/www/data/products/modules/cron.tsx +++ b/apps/www/data/products/modules/cron.tsx @@ -8,7 +8,7 @@ export default () => ({ metaDescription: 'Supabase Cron is a Postgres Module that uses the pg_cron database extension to manage recurring tasks. Manage your Cron Jobs using any Postgres tooling.', metaImage: '/images/modules/cron/og.png', - url: 'https://supabase.com/dashboard/project/_/integrations/cron-jobs/overview', + url: 'https://supabase.com/dashboard/project/_/integrations/cron/overview', docsUrl: '/docs/guides/cron', heroSection: { title: 'Supabase Cron', @@ -22,7 +22,7 @@ export default () => ({ icon: PRODUCT_MODULES['cron'].icon[24], cta: { label: 'Schedule your first Job', - link: 'https://supabase.com/dashboard/project/_/integrations/cron-jobs/overview', + link: 'https://supabase.com/dashboard/project/_/integrations/cron/overview', }, secondaryCta: { label: 'Explore documentation', From 4bc9596dbd847c4c6c981deeaa54bf1f8f1439f9 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Tue, 18 Feb 2025 11:58:22 +1000 Subject: [PATCH 18/18] consider js comments in assistant props (#33669) --- .../studio/components/ui/AIAssistantPanel/MessageMarkdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/ui/AIAssistantPanel/MessageMarkdown.tsx b/apps/studio/components/ui/AIAssistantPanel/MessageMarkdown.tsx index d7a38e9c464..019eab1197a 100644 --- a/apps/studio/components/ui/AIAssistantPanel/MessageMarkdown.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/MessageMarkdown.tsx @@ -151,7 +151,7 @@ export const MarkdownPre = ({ children }: { children: any }) => { const language = children[0].props.className?.replace('language-', '') || 'sql' const rawContent = children[0].props.children[0] - const propsMatch = rawContent.match(/--\s*props:\s*(\{[^}]+\})/) + const propsMatch = rawContent.match(/(?:--|\/\/)\s*props:\s*(\{[^}]+\})/) const snippetProps: AssistantSnippetProps = useMemo( () => (propsMatch ? JSON.parse(propsMatch[1]) : {}), @@ -164,7 +164,7 @@ export const MarkdownPre = ({ children }: { children: any }) => { const runQuery = snippetProps.runQuery === 'true' // Strip props from the content for both SQL and edge functions - const cleanContent = rawContent.replace(/--\s*props:\s*\{[^}]+\}/, '').trim() + const cleanContent = rawContent.replace(/(?:--|\/\/)\s*props:\s*\{[^}]+\}/, '').trim() const isDraggableToReports = supportSQLBlocks && canCreateSQLSnippet && router.pathname.endsWith('/reports/[id]')