diff --git a/.github/workflows/pg-meta-tests.yml b/.github/workflows/pg-meta-tests.yml index 23cc250212a..0731a65c8bc 100644 --- a/.github/workflows/pg-meta-tests.yml +++ b/.github/workflows/pg-meta-tests.yml @@ -5,10 +5,12 @@ on: branches: ['master'] paths: - 'packages/pg-meta/**/*' + - 'apps/studio/data/**/*' pull_request: branches: ['master'] paths: - 'packages/pg-meta/**/*' + - 'apps/studio/data/**/*' # Cancel old builds on new commit for same workflow + branch/PR concurrency: @@ -29,6 +31,7 @@ jobs: sparse-checkout: | packages/pg-meta packages/tsconfig + apps/studio/data - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 name: Install pnpm diff --git a/packages/pg-meta/test/sql/studio/get-users-count.test.ts b/packages/pg-meta/test/sql/studio/get-users-count.test.ts new file mode 100644 index 00000000000..cfabcee0666 --- /dev/null +++ b/packages/pg-meta/test/sql/studio/get-users-count.test.ts @@ -0,0 +1,258 @@ +import { randomUUID } from 'crypto' +import { afterAll, expect, test } from 'vitest' + +import { getUsersCountSQL } from 'studio-data/auth/auth.sql' +import { cleanupRoot, createDatabaseWithAuthSchema, createTestDatabase } from '../../db/utils' + +afterAll(async () => { + await cleanupRoot() +}) + +const withTestDatabase = ( + name: string, + fn: (db: Awaited>) => Promise +) => { + test(name, async () => { + const db = await createTestDatabase() + + // Initialize minimal auth schema needed for user count tests + await createDatabaseWithAuthSchema(db) + + try { + await fn(db) + } finally { + await db.cleanup() + } + }) +} + +withTestDatabase('returns correct count with no filters', async ({ executeQuery }) => { + // Insert 5 test users + const users = Array.from({ length: 5 }, (_, i) => ({ + id: randomUUID(), + email: `user${i + 1}@supabase.io`, + instance_id: '00000000-0000-0000-0000-000000000000', + aud: 'authenticated', + role: 'authenticated', + })) + + const values = users + .map( + (user) => + `('${user.id}', '${user.email}', '${user.instance_id}', '${user.aud}', '${user.role}', NOW())` + ) + .join(', ') + + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, aud, "role", created_at) + VALUES ${values} + `) + + const userCountSql = getUsersCountSQL({ forceExactCount: true }) + const result = await executeQuery>(userCountSql) + + expect(result[0].count).toBe(5) + expect(result[0].is_estimate).toBe(false) +}) + +withTestDatabase('filters verified users correctly', async ({ executeQuery }) => { + const now = new Date().toISOString() + + // 2 verified users (email confirmed) + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, email_confirmed_at, created_at) + VALUES + ('${randomUUID()}', 'verified1@supabase.io', '00000000-0000-0000-0000-000000000000', '${now}', NOW()), + ('${randomUUID()}', 'verified2@supabase.io', '00000000-0000-0000-0000-000000000000', '${now}', NOW()) + `) + + // 1 verified user (phone confirmed) + await executeQuery(` + INSERT INTO auth.users (id, phone, instance_id, phone_confirmed_at, created_at) + VALUES ('${randomUUID()}', '+1234567890', '00000000-0000-0000-0000-000000000000', '${now}', NOW()) + `) + + // 2 unverified users + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, created_at) + VALUES + ('${randomUUID()}', 'unverified1@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', 'unverified2@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + // Count verified users + const verifiedSql = getUsersCountSQL({ filter: 'verified', forceExactCount: true }) + const verifiedResult = await executeQuery>(verifiedSql) + expect(verifiedResult[0].count).toBe(3) + + // Count unverified users + const unverifiedSql = getUsersCountSQL({ filter: 'unverified', forceExactCount: true }) + const unverifiedResult = await executeQuery>(unverifiedSql) + expect(unverifiedResult[0].count).toBe(2) +}) + +withTestDatabase('filters anonymous users correctly', async ({ executeQuery }) => { + // 2 anonymous users + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, is_anonymous, created_at) + VALUES + ('${randomUUID()}', 'anon1@supabase.io', '00000000-0000-0000-0000-000000000000', true, NOW()), + ('${randomUUID()}', 'anon2@supabase.io', '00000000-0000-0000-0000-000000000000', true, NOW()) + `) + + // 3 regular users + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, is_anonymous, created_at) + VALUES + ('${randomUUID()}', 'regular1@supabase.io', '00000000-0000-0000-0000-000000000000', false, NOW()), + ('${randomUUID()}', 'regular2@supabase.io', '00000000-0000-0000-0000-000000000000', false, NOW()), + ('${randomUUID()}', 'regular3@supabase.io', '00000000-0000-0000-0000-000000000000', false, NOW()) + `) + + const anonSql = getUsersCountSQL({ filter: 'anonymous', forceExactCount: true }) + const result = await executeQuery>(anonSql) + expect(result[0].count).toBe(2) +}) + +withTestDatabase('searches by keywords across multiple fields', async ({ executeQuery }) => { + const searchUserId = randomUUID() + + await executeQuery(` + INSERT INTO auth.users (id, email, phone, instance_id, created_at) + VALUES + ('${searchUserId}', 'john.doe@supabase.io', NULL, '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', 'jane.smith@supabase.io', '+1234567890', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', 'bob.jones@supabase.io', '+9876543210', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + // Search by email keyword + const emailSearchSql = getUsersCountSQL({ keywords: 'john', forceExactCount: true }) + const emailResult = await executeQuery>(emailSearchSql) + expect(emailResult[0].count).toBe(1) + + // Search by phone keyword + const phoneSearchSql = getUsersCountSQL({ keywords: '1234', forceExactCount: true }) + const phoneResult = await executeQuery>(phoneSearchSql) + expect(phoneResult[0].count).toBe(1) + + // Search by ID (partial UUID) + const idPrefix = searchUserId.substring(0, 8) + const idSearchSql = getUsersCountSQL({ keywords: idPrefix, forceExactCount: true }) + const idResult = await executeQuery>(idSearchSql) + expect(idResult[0].count).toBe(1) +}) + +withTestDatabase('filters by provider correctly', async ({ executeQuery }) => { + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, raw_app_meta_data, created_at) + VALUES + ('${randomUUID()}', 'google1@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["google"]}', NOW()), + ('${randomUUID()}', 'google2@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["google"]}', NOW()), + ('${randomUUID()}', 'github@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["github"]}', NOW()), + ('${randomUUID()}', 'email@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["email"]}', NOW()) + `) + + // Filter by google provider + const googleSql = getUsersCountSQL({ providers: ['google'], forceExactCount: true }) + const googleResult = await executeQuery>(googleSql) + expect(googleResult[0].count).toBe(2) + + // Filter by multiple providers + const multipleSql = getUsersCountSQL({ + providers: ['google', 'github'], + forceExactCount: true, + }) + const multipleResult = await executeQuery>(multipleSql) + expect(multipleResult[0].count).toBe(3) +}) + +withTestDatabase('combines multiple filters correctly', async ({ executeQuery }) => { + const now = new Date().toISOString() + + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, email_confirmed_at, raw_app_meta_data, created_at) + VALUES + ('${randomUUID()}', 'verified.google@supabase.io', '00000000-0000-0000-0000-000000000000', '${now}', '{"providers": ["google"]}', NOW()), + ('${randomUUID()}', 'verified.github@supabase.io', '00000000-0000-0000-0000-000000000000', '${now}', '{"providers": ["github"]}', NOW()), + ('${randomUUID()}', 'unverified.google@supabase.io', '00000000-0000-0000-0000-000000000000', NULL, '{"providers": ["google"]}', NOW()) + `) + + // Combine verified filter with provider and keyword + const combinedSql = getUsersCountSQL({ + filter: 'verified', + providers: ['google'], + keywords: 'verified', + forceExactCount: true, + }) + const result = await executeQuery>(combinedSql) + expect(result[0].count).toBe(1) +}) + +withTestDatabase('optimized email search works correctly', async ({ executeQuery }) => { + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, created_at) + VALUES + ('${randomUUID()}', 'alice@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', 'bob@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', 'alicia@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + // Optimized search by email prefix + const emailSql = getUsersCountSQL({ column: 'email', keywords: 'ali', forceExactCount: true }) + const result = await executeQuery>(emailSql) + expect(result[0].count).toBe(2) // alice and alicia +}) + +withTestDatabase('optimized phone search works correctly', async ({ executeQuery }) => { + await executeQuery(` + INSERT INTO auth.users (id, phone, instance_id, created_at) + VALUES + ('${randomUUID()}', '+1234567890', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', '+1234999999', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', '+9876543210', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + // Optimized search by phone prefix + const phoneSql = getUsersCountSQL({ column: 'phone', keywords: '+1234', forceExactCount: true }) + const result = await executeQuery>(phoneSql) + expect(result[0].count).toBe(2) +}) + +withTestDatabase('optimized id search works correctly', async ({ executeQuery }) => { + const userId1 = 'abc12300-0000-4000-8000-000000000001' + const userId2 = 'abc12300-0000-4000-8000-000000000002' + const userId3 = 'def45600-0000-4000-8000-000000000003' + + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, created_at) + VALUES + ('${userId1}', 'user1@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${userId2}', 'user2@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${userId3}', 'user3@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + // Optimized search by id prefix + const idSql = getUsersCountSQL({ column: 'id', keywords: 'abc123', forceExactCount: true }) + const result = await executeQuery>(idSql) + expect(result[0].count).toBe(2) + + // Exact UUID match + const exactSql = getUsersCountSQL({ column: 'id', keywords: userId1, forceExactCount: true }) + const exactResult = await executeQuery>(exactSql) + expect(exactResult[0].count).toBe(1) +}) + +withTestDatabase('handles SAML provider filtering', async ({ executeQuery }) => { + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, raw_app_meta_data, created_at) + VALUES + ('${randomUUID()}', 'sso1@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["sso:provider-id-1"]}', NOW()), + ('${randomUUID()}', 'sso2@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["sso:provider-id-2"]}', NOW()), + ('${randomUUID()}', 'google@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["google"]}', NOW()) + `) + + // Filter by SAML provider (special handling for sso: prefix) + const samlSql = getUsersCountSQL({ providers: ['saml 2.0'], forceExactCount: true }) + const result = await executeQuery>(samlSql) + expect(result[0].count).toBe(2) +}) diff --git a/packages/pg-meta/test/sql/studio/get-users-paginated.test.ts b/packages/pg-meta/test/sql/studio/get-users-paginated.test.ts new file mode 100644 index 00000000000..e2ae24fcfb9 --- /dev/null +++ b/packages/pg-meta/test/sql/studio/get-users-paginated.test.ts @@ -0,0 +1,343 @@ +import { randomUUID } from 'crypto' +import { afterAll, expect, test } from 'vitest' + +import { getPaginatedUsersSQL } from 'studio-data/auth/auth.sql' +import { cleanupRoot, createDatabaseWithAuthSchema, createTestDatabase } from '../../db/utils' + +afterAll(async () => { + await cleanupRoot() +}) + +const withTestDatabase = ( + name: string, + fn: (db: Awaited>) => Promise +) => { + test(name, async () => { + const db = await createTestDatabase() + + // Initialize minimal auth schema needed for user pagination tests + await createDatabaseWithAuthSchema(db, { includeIdentities: true }) + + try { + await fn(db) + } finally { + await db.cleanup() + } + }) +} + +withTestDatabase('returns paginated users with default settings', async ({ executeQuery }) => { + // Insert 10 users with different created_at times + for (let i = 0; i < 10; i++) { + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, created_at) + VALUES ( + '${randomUUID()}', + 'user${i}@supabase.io', + '00000000-0000-0000-0000-000000000000', + NOW() + INTERVAL '${i} minutes' + ) + `) + } + + // Get first page (default limit 50, page 0) + const sql = getPaginatedUsersSQL({ sort: 'created_at', order: 'desc' }) + const result = await executeQuery>(sql) + + expect(result.length).toBe(10) + // Should be ordered by created_at desc, so user9 should be first + expect(result[0].email).toBe('user9@supabase.io') + expect(result[9].email).toBe('user0@supabase.io') +}) + +withTestDatabase('respects custom limit and pagination', async ({ executeQuery }) => { + // Insert 30 users + for (let i = 0; i < 30; i++) { + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, created_at) + VALUES ( + '${randomUUID()}', + 'user${String(i).padStart(2, '0')}@supabase.io', + '00000000-0000-0000-0000-000000000000', + NOW() + INTERVAL '${i} minutes' + ) + `) + } + + // Get page 1 with limit 10 (should skip first 10, get next 10) + const sql = getPaginatedUsersSQL({ sort: 'created_at', order: 'desc', limit: 10, page: 1 }) + const result = await executeQuery>(sql) + + expect(result.length).toBe(10) + // Page 1 should start at user19 (0-indexed, skipping first 10) + expect(result[0].email).toBe('user19@supabase.io') + expect(result[9].email).toBe('user10@supabase.io') +}) + +withTestDatabase('sorts by email in ascending order', async ({ executeQuery }) => { + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, created_at) + VALUES + ('${randomUUID()}', 'charlie@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', 'alice@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', 'bob@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + const sql = getPaginatedUsersSQL({ sort: 'email', order: 'asc', limit: 10 }) + const result = await executeQuery>(sql) + + expect(result.length).toBe(3) + expect(result[0].email).toBe('alice@supabase.io') + expect(result[1].email).toBe('bob@supabase.io') + expect(result[2].email).toBe('charlie@supabase.io') +}) + +withTestDatabase('filters by keywords across multiple fields', async ({ executeQuery }) => { + const searchUserId = randomUUID() + + await executeQuery(` + INSERT INTO auth.users (id, email, phone, instance_id, raw_user_meta_data, created_at) + VALUES + ('${searchUserId}', 'john.doe@supabase.io', NULL, '00000000-0000-0000-0000-000000000000', '{"full_name": "John Doe"}', NOW()), + ('${randomUUID()}', 'jane.smith@supabase.io', '+1234567890', '00000000-0000-0000-0000-000000000000', '{"full_name": "Jane Smith"}', NOW()), + ('${randomUUID()}', 'bob.jones@supabase.io', '+9876543210', '00000000-0000-0000-0000-000000000000', '{"full_name": "Bob Jones"}', NOW()) + `) + + // Search by keyword "john" - should find john.doe by email + const emailSql = getPaginatedUsersSQL({ sort: 'created_at', order: 'desc', keywords: 'john' }) + const emailResult = await executeQuery>(emailSql) + expect(emailResult.length).toBe(1) + expect(emailResult[0].email).toBe('john.doe@supabase.io') + + // Search by phone + const phoneSql = getPaginatedUsersSQL({ sort: 'created_at', order: 'desc', keywords: '1234' }) + const phoneResult = await executeQuery>(phoneSql) + expect(phoneResult.length).toBe(1) + expect(phoneResult[0].phone).toBe('+1234567890') + + // Search by ID prefix + const idPrefix = searchUserId.substring(0, 8) + const idSql = getPaginatedUsersSQL({ sort: 'created_at', order: 'desc', keywords: idPrefix }) + const idResult = await executeQuery>(idSql) + expect(idResult.length).toBe(1) + expect(idResult[0].email).toBe('john.doe@supabase.io') +}) + +withTestDatabase('filters verified and unverified users', async ({ executeQuery }) => { + const now = new Date().toISOString() + + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, email_confirmed_at, created_at) + VALUES + ('${randomUUID()}', 'verified1@supabase.io', '00000000-0000-0000-0000-000000000000', '${now}', NOW()), + ('${randomUUID()}', 'verified2@supabase.io', '00000000-0000-0000-0000-000000000000', '${now}', NOW()), + ('${randomUUID()}', 'unverified@supabase.io', '00000000-0000-0000-0000-000000000000', NULL, NOW()) + `) + + // Filter verified users + const verifiedSql = getPaginatedUsersSQL({ + sort: 'created_at', + order: 'desc', + verified: 'verified', + }) + const verifiedResult = await executeQuery>(verifiedSql) + expect(verifiedResult.length).toBe(2) + + // Filter unverified users + const unverifiedSql = getPaginatedUsersSQL({ + sort: 'created_at', + order: 'desc', + verified: 'unverified', + }) + const unverifiedResult = await executeQuery>(unverifiedSql) + expect(unverifiedResult.length).toBe(1) + expect(unverifiedResult[0].email).toBe('unverified@supabase.io') +}) + +withTestDatabase('filters anonymous users', async ({ executeQuery }) => { + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, is_anonymous, created_at) + VALUES + ('${randomUUID()}', 'anon1@supabase.io', '00000000-0000-0000-0000-000000000000', true, NOW()), + ('${randomUUID()}', 'anon2@supabase.io', '00000000-0000-0000-0000-000000000000', true, NOW()), + ('${randomUUID()}', 'regular@supabase.io', '00000000-0000-0000-0000-000000000000', false, NOW()) + `) + + const sql = getPaginatedUsersSQL({ sort: 'created_at', order: 'desc', verified: 'anonymous' }) + const result = await executeQuery>(sql) + expect(result.length).toBe(2) +}) + +withTestDatabase('filters by providers', async ({ executeQuery }) => { + const user1 = randomUUID() + const user2 = randomUUID() + const user3 = randomUUID() + + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, raw_app_meta_data, created_at) + VALUES + ('${user1}', 'google1@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["google"]}', NOW()), + ('${user2}', 'google2@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["google"]}', NOW()), + ('${user3}', 'github@supabase.io', '00000000-0000-0000-0000-000000000000', '{"providers": ["github"]}', NOW()) + `) + + // Also insert identities for the users + await executeQuery(` + INSERT INTO auth.identities (id, user_id, provider, identity_data, created_at) + VALUES + ('google1', '${user1}', 'google', '{}', NOW()), + ('google2', '${user2}', 'google', '{}', NOW()), + ('github1', '${user3}', 'github', '{}', NOW()) + `) + + // Filter by google provider + const googleSql = getPaginatedUsersSQL({ + sort: 'created_at', + order: 'desc', + providers: ['google'], + }) + const googleResult = await executeQuery>(googleSql) + expect(googleResult.length).toBe(2) + + // Filter by multiple providers + const multipleSql = getPaginatedUsersSQL({ + sort: 'created_at', + order: 'desc', + providers: ['google', 'github'], + }) + const multipleResult = await executeQuery>(multipleSql) + expect(multipleResult.length).toBe(3) +}) + +withTestDatabase('combines multiple filters', async ({ executeQuery }) => { + const now = new Date().toISOString() + + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, email_confirmed_at, raw_app_meta_data, created_at) + VALUES + ('${randomUUID()}', 'verified.google@supabase.io', '00000000-0000-0000-0000-000000000000', '${now}', '{"providers": ["google"]}', NOW()), + ('${randomUUID()}', 'verified.github@supabase.io', '00000000-0000-0000-0000-000000000000', '${now}', '{"providers": ["github"]}', NOW()), + ('${randomUUID()}', 'unverified.google@supabase.io', '00000000-0000-0000-0000-000000000000', NULL, '{"providers": ["google"]}', NOW()) + `) + + // Combine verified filter with provider and keyword + const sql = getPaginatedUsersSQL({ + sort: 'created_at', + order: 'desc', + keywords: 'verified', + verified: 'verified', + providers: ['google'], + }) + const result = await executeQuery>(sql) + expect(result.length).toBe(1) + expect(result[0].email).toBe('verified.google@supabase.io') +}) + +withTestDatabase('optimized email search works correctly', async ({ executeQuery }) => { + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, created_at) + VALUES + ('${randomUUID()}', 'alice@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', 'alicia@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', 'bob@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + // Optimized search by email prefix + const sql = getPaginatedUsersSQL({ + sort: 'created_at', + order: 'desc', + column: 'email', + keywords: 'ali', + limit: 10, + }) + const result = await executeQuery>(sql) + expect(result.length).toBe(2) + expect(result[0].email).toBe('alice@supabase.io') + expect(result[1].email).toBe('alicia@supabase.io') +}) + +withTestDatabase('optimized phone search works correctly', async ({ executeQuery }) => { + await executeQuery(` + INSERT INTO auth.users (id, phone, instance_id, created_at) + VALUES + ('${randomUUID()}', '+1234567890', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', '+1234999999', '00000000-0000-0000-0000-000000000000', NOW()), + ('${randomUUID()}', '+9876543210', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + // Optimized search by phone prefix + const sql = getPaginatedUsersSQL({ + sort: 'created_at', + order: 'desc', + column: 'phone', + keywords: '+1234', + limit: 10, + }) + const result = await executeQuery>(sql) + expect(result.length).toBe(2) +}) + +withTestDatabase('optimized id search works correctly', async ({ executeQuery }) => { + const userId1 = 'abc12300-0000-4000-8000-000000000001' + const userId2 = 'abc12300-0000-4000-8000-000000000002' + const userId3 = 'def45600-0000-4000-8000-000000000003' + + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, created_at) + VALUES + ('${userId1}', 'user1@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${userId2}', 'user2@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()), + ('${userId3}', 'user3@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + // Optimized search by id prefix + const sql = getPaginatedUsersSQL({ + sort: 'created_at', + order: 'desc', + column: 'id', + keywords: 'abc123', + limit: 10, + }) + const result = await executeQuery>(sql) + expect(result.length).toBe(2) + expect(result[0].id).toBe(userId1) + expect(result[1].id).toBe(userId2) + + // Exact UUID match + const exactSql = getPaginatedUsersSQL({ + sort: 'created_at', + order: 'desc', + column: 'id', + keywords: userId1, + limit: 10, + }) + const exactResult = await executeQuery>(exactSql) + expect(exactResult.length).toBe(1) + expect(exactResult[0].id).toBe(userId1) +}) + +withTestDatabase('includes provider information from identities', async ({ executeQuery }) => { + const userId = randomUUID() + + await executeQuery(` + INSERT INTO auth.users (id, email, instance_id, created_at) + VALUES ('${userId}', 'user@supabase.io', '00000000-0000-0000-0000-000000000000', NOW()) + `) + + // Insert multiple identities for the user + await executeQuery(` + INSERT INTO auth.identities (id, user_id, provider, identity_data, created_at) + VALUES + ('google1', '${userId}', 'google', '{}', NOW()), + ('github1', '${userId}', 'github', '{}', NOW()) + `) + + const sql = getPaginatedUsersSQL({ sort: 'created_at', order: 'desc', limit: 10 }) + const result = await executeQuery>(sql) + + expect(result.length).toBe(1) + expect(result[0].email).toBe('user@supabase.io') + expect(result[0].providers).toHaveLength(2) + expect(result[0].providers).toContain('google') + expect(result[0].providers).toContain('github') +}) diff --git a/packages/pg-meta/vitest.config.ts b/packages/pg-meta/vitest.config.ts new file mode 100644 index 00000000000..9c0c48a920f --- /dev/null +++ b/packages/pg-meta/vitest.config.ts @@ -0,0 +1,10 @@ +import { resolve } from 'node:path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + 'studio-data': resolve(__dirname, '../../apps/studio/data'), + }, + }, +})