diff --git a/CLAUDE.md b/CLAUDE.md index cb6b602e5..a09c48d19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -160,4 +160,10 @@ export async function getWorkspaceSettingsHandler( ### Authentication and User Context - **Use requireAuth middleware** - Apply to all protected routes - **Extract user context** - Use `c.get('busterUser')` to get the authenticated user -- **Type as User** - Import `User` type from `@buster/database` for handler parameters \ No newline at end of file +- **Type as User** - Import `User` type from `@buster/database` for handler parameters + +## Database Operations + +### Soft Delete and Upsert Practices +- In our database, we never hard delete, we always use soft deletes with the `deleted_at` field +- For update operations, we should almost always perform an upsert unless otherwise specified \ No newline at end of file diff --git a/apps/server/src/api/v2/security/add-approved-domains.int.test.ts b/apps/server/src/api/v2/security/add-approved-domains.int.test.ts index 2498acd87..503469e39 100644 --- a/apps/server/src/api/v2/security/add-approved-domains.int.test.ts +++ b/apps/server/src/api/v2/security/add-approved-domains.int.test.ts @@ -1,15 +1,15 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import type { Organization, User } from '@buster/database'; +import { HTTPException } from 'hono/http-exception'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { addApprovedDomainsHandler } from './add-approved-domains'; import { - createTestUserInDb, - createTestOrganizationInDb, - createTestOrgMemberInDb, - cleanupTestUser, cleanupTestOrganization, + cleanupTestUser, + createTestOrgMemberInDb, + createTestOrganizationInDb, + createTestUserInDb, getOrganizationFromDb, } from './test-db-utils'; -import { HTTPException } from 'hono/http-exception'; -import type { User, Organization } from '@buster/database'; describe('addApprovedDomainsHandler (integration)', () => { let testUser: User; @@ -37,9 +37,9 @@ describe('addApprovedDomainsHandler (integration)', () => { const result = await addApprovedDomainsHandler(request, testUser); expect(result).toHaveLength(3); - expect(result.map(d => d.domain)).toContain('existing.com'); - expect(result.map(d => d.domain)).toContain('new.com'); - expect(result.map(d => d.domain)).toContain('another.com'); + expect(result.map((d) => d.domain)).toContain('existing.com'); + expect(result.map((d) => d.domain)).toContain('new.com'); + expect(result.map((d) => d.domain)).toContain('another.com'); // Verify database was updated const updatedOrg = await getOrganizationFromDb(testOrg.id); @@ -56,8 +56,8 @@ describe('addApprovedDomainsHandler (integration)', () => { const result = await addApprovedDomainsHandler(request, testUser); expect(result).toHaveLength(2); - expect(result.map(d => d.domain)).toContain('first.com'); - expect(result.map(d => d.domain)).toContain('second.com'); + expect(result.map((d) => d.domain)).toContain('first.com'); + expect(result.map((d) => d.domain)).toContain('second.com'); const updatedOrg = await getOrganizationFromDb(orgWithNoDomains.id); expect(updatedOrg?.domains).toEqual(['first.com', 'second.com']); @@ -72,7 +72,7 @@ describe('addApprovedDomainsHandler (integration)', () => { const result = await addApprovedDomainsHandler(request, testUser); expect(result).toHaveLength(3); - const domains = result.map(d => d.domain); + const domains = result.map((d) => d.domain); expect(domains).toContain('existing.com'); expect(domains).toContain('new.com'); expect(domains).toContain('test.io'); @@ -85,7 +85,7 @@ describe('addApprovedDomainsHandler (integration)', () => { const result = await addApprovedDomainsHandler(request, testUser); expect(result).toHaveLength(2); - const domains = result.map(d => d.domain); + const domains = result.map((d) => d.domain); expect(domains).toEqual(['existing.com', 'new.com']); const updatedOrg = await getOrganizationFromDb(testOrg.id); @@ -150,7 +150,7 @@ describe('addApprovedDomainsHandler (integration)', () => { await createTestOrgMemberInDb(testUser.id, testOrg.id, 'workspace_admin'); const originalUpdatedAt = testOrg.updatedAt; - await new Promise(resolve => setTimeout(resolve, 10)); // Ensure time difference + await new Promise((resolve) => setTimeout(resolve, 10)); // Ensure time difference const request = { domains: ['new.com'] }; await addApprovedDomainsHandler(request, testUser); @@ -162,4 +162,4 @@ describe('addApprovedDomainsHandler (integration)', () => { ); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/add-approved-domains.test.ts b/apps/server/src/api/v2/security/add-approved-domains.test.ts index 055e1a9da..4624de641 100644 --- a/apps/server/src/api/v2/security/add-approved-domains.test.ts +++ b/apps/server/src/api/v2/security/add-approved-domains.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { addApprovedDomainsHandler } from './add-approved-domains'; -import { createTestUser, createTestOrganization } from './test-fixtures'; -import * as securityUtils from './security-utils'; -import { DomainService } from './domain-service'; import { db } from '@buster/database'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { addApprovedDomainsHandler } from './add-approved-domains'; +import { DomainService } from './domain-service'; +import * as securityUtils from './security-utils'; +import { createTestOrganization, createTestUser } from './test-fixtures'; // Mock dependencies vi.mock('./security-utils'); @@ -32,22 +32,22 @@ describe('addApprovedDomainsHandler', () => { domains: ['existing.com'], }); const mockOrgMembership = { organizationId: 'org-123', role: 'workspace_admin' }; - + beforeEach(() => { vi.clearAllMocks(); - + // Setup default mocks vi.mocked(securityUtils.validateUserOrganization).mockResolvedValue(mockOrgMembership); vi.mocked(securityUtils.checkAdminPermissions).mockImplementation(() => {}); vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(mockOrg); - + // Setup domain service mocks vi.mocked(DomainService.prototype.mergeDomains).mockReturnValue(['existing.com', 'new.com']); vi.mocked(DomainService.prototype.formatDomainsResponse).mockReturnValue([ { domain: 'existing.com', created_at: mockOrg.createdAt }, { domain: 'new.com', created_at: mockOrg.createdAt }, ]); - + // Mock database update const mockDbChain = { update: vi.fn().mockReturnThis(), @@ -59,19 +59,22 @@ describe('addApprovedDomainsHandler', () => { it('should add new domains successfully', async () => { const request = { domains: ['new.com'] }; - + const result = await addApprovedDomainsHandler(request, mockUser); - + expect(securityUtils.validateUserOrganization).toHaveBeenCalledWith(mockUser.id); expect(securityUtils.checkAdminPermissions).toHaveBeenCalledWith('workspace_admin'); expect(securityUtils.fetchOrganization).toHaveBeenCalledWith('org-123'); - - expect(DomainService.prototype.mergeDomains).toHaveBeenCalledWith(['existing.com'], ['new.com']); + + expect(DomainService.prototype.mergeDomains).toHaveBeenCalledWith( + ['existing.com'], + ['new.com'] + ); expect(DomainService.prototype.formatDomainsResponse).toHaveBeenCalledWith( ['existing.com', 'new.com'], mockOrg.createdAt ); - + expect(result).toEqual([ { domain: 'existing.com', created_at: mockOrg.createdAt }, { domain: 'new.com', created_at: mockOrg.createdAt }, @@ -81,15 +84,15 @@ describe('addApprovedDomainsHandler', () => { it('should handle empty existing domains', async () => { const orgWithNoDomains = { ...mockOrg, domains: null }; vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(orgWithNoDomains); - + vi.mocked(DomainService.prototype.mergeDomains).mockReturnValue(['new.com']); vi.mocked(DomainService.prototype.formatDomainsResponse).mockReturnValue([ { domain: 'new.com', created_at: mockOrg.createdAt }, ]); - + const request = { domains: ['new.com'] }; const result = await addApprovedDomainsHandler(request, mockUser); - + expect(DomainService.prototype.mergeDomains).toHaveBeenCalledWith([], ['new.com']); expect(result).toEqual([{ domain: 'new.com', created_at: mockOrg.createdAt }]); }); @@ -102,9 +105,9 @@ describe('addApprovedDomainsHandler', () => { where: vi.fn().mockResolvedValue(undefined), }; vi.mocked(db.update).mockReturnValue(mockDbChain as any); - + await addApprovedDomainsHandler(request, mockUser); - + expect(db.update).toHaveBeenCalled(); expect(mockDbChain.set).toHaveBeenCalledWith({ domains: ['existing.com', 'new.com'], @@ -116,9 +119,9 @@ describe('addApprovedDomainsHandler', () => { vi.mocked(securityUtils.validateUserOrganization).mockRejectedValue( new Error('User not in organization') ); - + const request = { domains: ['new.com'] }; - + await expect(addApprovedDomainsHandler(request, mockUser)).rejects.toThrow( 'User not in organization' ); @@ -128,11 +131,11 @@ describe('addApprovedDomainsHandler', () => { vi.mocked(securityUtils.checkAdminPermissions).mockImplementation(() => { throw new Error('Insufficient permissions'); }); - + const request = { domains: ['new.com'] }; - + await expect(addApprovedDomainsHandler(request, mockUser)).rejects.toThrow( 'Insufficient permissions' ); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/add-approved-domains.ts b/apps/server/src/api/v2/security/add-approved-domains.ts index 6397b15ed..1a7dee4ed 100644 --- a/apps/server/src/api/v2/security/add-approved-domains.ts +++ b/apps/server/src/api/v2/security/add-approved-domains.ts @@ -1,14 +1,14 @@ +import { type User, and, db, eq, isNull, organizations } from '@buster/database'; import type { AddApprovedDomainRequest, AddApprovedDomainsResponse, } from '@buster/server-shared/security'; -import { type User, db, organizations, eq, and, isNull } from '@buster/database'; -import { - validateUserOrganization, - fetchOrganization, - checkAdminPermissions -} from './security-utils'; import { DomainService } from './domain-service'; +import { + checkAdminPermissions, + fetchOrganization, + validateUserOrganization, +} from './security-utils'; const domainService = new DomainService(); @@ -23,7 +23,7 @@ export async function addApprovedDomainsHandler( // Fetch current organization const org = await fetchOrganization(userOrg.organizationId); const currentDomains = org.domains || []; - + // Merge domains using domain service const updatedDomains = domainService.mergeDomains(currentDomains, request.domains); @@ -34,20 +34,12 @@ export async function addApprovedDomainsHandler( return domainService.formatDomainsResponse(updatedDomains, org.createdAt); } -async function updateOrganizationDomains( - organizationId: string, - domains: string[] -): Promise { +async function updateOrganizationDomains(organizationId: string, domains: string[]): Promise { await db .update(organizations) .set({ domains, updatedAt: new Date().toISOString(), }) - .where( - and( - eq(organizations.id, organizationId), - isNull(organizations.deletedAt) - ) - ); -} \ No newline at end of file + .where(and(eq(organizations.id, organizationId), isNull(organizations.deletedAt))); +} diff --git a/apps/server/src/api/v2/security/domain-service.test.ts b/apps/server/src/api/v2/security/domain-service.test.ts index c21ef91a8..bdac6f645 100644 --- a/apps/server/src/api/v2/security/domain-service.test.ts +++ b/apps/server/src/api/v2/security/domain-service.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { DomainService } from './domain-service'; describe('DomainService', () => { @@ -139,4 +139,4 @@ describe('DomainService', () => { ]); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/domain-service.ts b/apps/server/src/api/v2/security/domain-service.ts index e684fec86..cf0bd6cf8 100644 --- a/apps/server/src/api/v2/security/domain-service.ts +++ b/apps/server/src/api/v2/security/domain-service.ts @@ -1,25 +1,25 @@ export class DomainService { normalizeDomains(domains: string[]): string[] { - return domains.map(d => d.toLowerCase().trim()); + return domains.map((d) => d.toLowerCase().trim()); } mergeDomains(currentDomains: string[], newDomains: string[]): string[] { const normalizedCurrent = this.normalizeDomains(currentDomains); const normalizedNew = this.normalizeDomains(newDomains); - + // Filter out duplicates from new domains - const uniqueNew = normalizedNew.filter(d => !normalizedCurrent.includes(d)); - + const uniqueNew = normalizedNew.filter((d) => !normalizedCurrent.includes(d)); + // Return original current domains plus unique new ones return [...currentDomains, ...uniqueNew]; } filterDomains(currentDomains: string[], domainsToRemove: string[]): string[] { const normalizedToRemove = this.normalizeDomains(domainsToRemove); - + // Filter out domains that match (case-insensitive) return currentDomains.filter( - domain => !normalizedToRemove.includes(domain.toLowerCase().trim()) + (domain) => !normalizedToRemove.includes(domain.toLowerCase().trim()) ); } @@ -30,13 +30,13 @@ export class DomainService { if (!domains || domains.length === 0) { return []; } - + // Convert PostgreSQL timestamp to ISO string format const isoCreatedAt = new Date(createdAt).toISOString(); - - return domains.map(domain => ({ + + return domains.map((domain) => ({ domain, created_at: isoCreatedAt, })); } -} \ No newline at end of file +} diff --git a/apps/server/src/api/v2/security/get-approved-domains.int.test.ts b/apps/server/src/api/v2/security/get-approved-domains.int.test.ts index a16f9d3de..5248fd455 100644 --- a/apps/server/src/api/v2/security/get-approved-domains.int.test.ts +++ b/apps/server/src/api/v2/security/get-approved-domains.int.test.ts @@ -1,15 +1,15 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { db, eq, organizations } from '@buster/database'; +import type { Organization, User } from '@buster/database'; +import { HTTPException } from 'hono/http-exception'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { getApprovedDomainsHandler } from './get-approved-domains'; import { - createTestUserInDb, - createTestOrganizationInDb, - createTestOrgMemberInDb, - cleanupTestUser, cleanupTestOrganization, + cleanupTestUser, + createTestOrgMemberInDb, + createTestOrganizationInDb, + createTestUserInDb, } from './test-db-utils'; -import { HTTPException } from 'hono/http-exception'; -import { db, organizations, eq } from '@buster/database'; -import type { User, Organization } from '@buster/database'; describe('getApprovedDomainsHandler (integration)', () => { let testUser: User; @@ -36,8 +36,8 @@ describe('getApprovedDomainsHandler (integration)', () => { const result = await getApprovedDomainsHandler(testUser); expect(result).toHaveLength(2); - expect(result.map(d => d.domain)).toContain('example.com'); - expect(result.map(d => d.domain)).toContain('test.io'); + expect(result.map((d) => d.domain)).toContain('example.com'); + expect(result.map((d) => d.domain)).toContain('test.io'); }); it('should return empty array for org with no domains', async () => { @@ -76,8 +76,8 @@ describe('getApprovedDomainsHandler (integration)', () => { const result = await getApprovedDomainsHandler(roleUser); expect(result).toHaveLength(2); - expect(result.map(d => d.domain)).toContain('example.com'); - expect(result.map(d => d.domain)).toContain('test.io'); + expect(result.map((d) => d.domain)).toContain('example.com'); + expect(result.map((d) => d.domain)).toContain('test.io'); await cleanupTestUser(roleUser.id); } @@ -138,7 +138,9 @@ describe('getApprovedDomainsHandler (integration)', () => { .limit(1); expect(orgAfter[0]?.domains).toEqual(originalOrg.domains); - expect(new Date(orgAfter[0]?.updatedAt).toISOString()).toEqual(new Date(originalOrg.updatedAt).toISOString()); + expect(new Date(orgAfter[0]?.updatedAt).toISOString()).toEqual( + new Date(originalOrg.updatedAt).toISOString() + ); }); it('should return domains in the correct format', async () => { @@ -146,7 +148,7 @@ describe('getApprovedDomainsHandler (integration)', () => { const result = await getApprovedDomainsHandler(testUser); - result.forEach(domainEntry => { + result.forEach((domainEntry) => { expect(domainEntry).toHaveProperty('domain'); expect(domainEntry).toHaveProperty('created_at'); expect(typeof domainEntry.domain).toBe('string'); @@ -157,4 +159,4 @@ describe('getApprovedDomainsHandler (integration)', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/get-approved-domains.test.ts b/apps/server/src/api/v2/security/get-approved-domains.test.ts index 8f3b8d4a0..852d570c1 100644 --- a/apps/server/src/api/v2/security/get-approved-domains.test.ts +++ b/apps/server/src/api/v2/security/get-approved-domains.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { getApprovedDomainsHandler } from './get-approved-domains'; -import { createTestUser, createTestOrganization } from './test-fixtures'; -import * as securityUtils from './security-utils'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { DomainService } from './domain-service'; +import { getApprovedDomainsHandler } from './get-approved-domains'; +import * as securityUtils from './security-utils'; +import { createTestOrganization, createTestUser } from './test-fixtures'; // Mock dependencies vi.mock('./security-utils'); @@ -19,14 +19,14 @@ describe('getApprovedDomainsHandler', () => { domains: ['example.com', 'test.io'], }); const mockOrgMembership = { organizationId: 'org-123', role: 'member' }; - + beforeEach(() => { vi.clearAllMocks(); - + // Setup default mocks vi.mocked(securityUtils.validateUserOrganization).mockResolvedValue(mockOrgMembership); vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(mockOrg); - + // Setup domain service mocks vi.mocked(DomainService.prototype.formatDomainsResponse).mockReturnValue([ { domain: 'example.com', created_at: mockOrg.createdAt }, @@ -36,15 +36,15 @@ describe('getApprovedDomainsHandler', () => { it('should return domains for valid organization', async () => { const result = await getApprovedDomainsHandler(mockUser); - + expect(securityUtils.validateUserOrganization).toHaveBeenCalledWith(mockUser.id); expect(securityUtils.fetchOrganization).toHaveBeenCalledWith('org-123'); - + expect(DomainService.prototype.formatDomainsResponse).toHaveBeenCalledWith( ['example.com', 'test.io'], mockOrg.createdAt ); - + expect(result).toEqual([ { domain: 'example.com', created_at: mockOrg.createdAt }, { domain: 'test.io', created_at: mockOrg.createdAt }, @@ -54,12 +54,15 @@ describe('getApprovedDomainsHandler', () => { it('should return empty array for org with no domains', async () => { const orgWithNoDomains = { ...mockOrg, domains: null }; vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(orgWithNoDomains); - + vi.mocked(DomainService.prototype.formatDomainsResponse).mockReturnValue([]); - + const result = await getApprovedDomainsHandler(mockUser); - - expect(DomainService.prototype.formatDomainsResponse).toHaveBeenCalledWith(null, mockOrg.createdAt); + + expect(DomainService.prototype.formatDomainsResponse).toHaveBeenCalledWith( + null, + mockOrg.createdAt + ); expect(result).toEqual([]); }); @@ -67,33 +70,29 @@ describe('getApprovedDomainsHandler', () => { vi.mocked(securityUtils.validateUserOrganization).mockRejectedValue( new Error('User not in organization') ); - - await expect(getApprovedDomainsHandler(mockUser)).rejects.toThrow( - 'User not in organization' - ); + + await expect(getApprovedDomainsHandler(mockUser)).rejects.toThrow('User not in organization'); }); it('should handle organization fetch errors', async () => { vi.mocked(securityUtils.fetchOrganization).mockRejectedValue( new Error('Organization not found') ); - - await expect(getApprovedDomainsHandler(mockUser)).rejects.toThrow( - 'Organization not found' - ); + + await expect(getApprovedDomainsHandler(mockUser)).rejects.toThrow('Organization not found'); }); it('should not require admin permissions', async () => { // Test with non-admin role const nonAdminMembership = { organizationId: 'org-123', role: 'member' }; vi.mocked(securityUtils.validateUserOrganization).mockResolvedValue(nonAdminMembership); - + const result = await getApprovedDomainsHandler(mockUser); - + // Should still succeed expect(result).toEqual([ { domain: 'example.com', created_at: mockOrg.createdAt }, { domain: 'test.io', created_at: mockOrg.createdAt }, ]); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/get-approved-domains.ts b/apps/server/src/api/v2/security/get-approved-domains.ts index 064e60bd6..5bdddaa92 100644 --- a/apps/server/src/api/v2/security/get-approved-domains.ts +++ b/apps/server/src/api/v2/security/get-approved-domains.ts @@ -1,13 +1,11 @@ +import type { User } from '@buster/database'; import type { GetApprovedDomainsResponse } from '@buster/server-shared/security'; -import { type User } from '@buster/database'; -import { validateUserOrganization, fetchOrganization } from './security-utils'; import { DomainService } from './domain-service'; +import { fetchOrganization, validateUserOrganization } from './security-utils'; const domainService = new DomainService(); -export async function getApprovedDomainsHandler( - user: User -): Promise { +export async function getApprovedDomainsHandler(user: User): Promise { // Validate user organization const userOrg = await validateUserOrganization(user.id); @@ -16,4 +14,4 @@ export async function getApprovedDomainsHandler( // Return formatted domains response return domainService.formatDomainsResponse(org.domains, org.createdAt); -} \ No newline at end of file +} diff --git a/apps/server/src/api/v2/security/get-workspace-settings.int.test.ts b/apps/server/src/api/v2/security/get-workspace-settings.int.test.ts index 30a361fb2..c1322c652 100644 --- a/apps/server/src/api/v2/security/get-workspace-settings.int.test.ts +++ b/apps/server/src/api/v2/security/get-workspace-settings.int.test.ts @@ -1,15 +1,15 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { db, eq, organizations } from '@buster/database'; +import type { Organization, User } from '@buster/database'; +import { HTTPException } from 'hono/http-exception'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { getWorkspaceSettingsHandler } from './get-workspace-settings'; import { - createTestUserInDb, - createTestOrganizationInDb, - createTestOrgMemberInDb, - cleanupTestUser, cleanupTestOrganization, + cleanupTestUser, + createTestOrgMemberInDb, + createTestOrganizationInDb, + createTestUserInDb, } from './test-db-utils'; -import { HTTPException } from 'hono/http-exception'; -import { db, organizations, eq } from '@buster/database'; -import type { User, Organization } from '@buster/database'; describe('getWorkspaceSettingsHandler (integration)', () => { let testUser: User; @@ -160,7 +160,9 @@ describe('getWorkspaceSettingsHandler (integration)', () => { .where(eq(organizations.id, testOrg.id)) .limit(1); - expect(orgAfter[0]?.restrictNewUserInvitations).toEqual(originalOrg.restrictNewUserInvitations); + expect(orgAfter[0]?.restrictNewUserInvitations).toEqual( + originalOrg.restrictNewUserInvitations + ); expect(orgAfter[0]?.defaultRole).toEqual(originalOrg.defaultRole); expect(new Date(orgAfter[0]?.updatedAt).toISOString()).toEqual(originalOrg.updatedAt); }); @@ -191,4 +193,4 @@ describe('getWorkspaceSettingsHandler (integration)', () => { expect(Array.isArray(result.default_datasets)).toBe(true); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/get-workspace-settings.test.ts b/apps/server/src/api/v2/security/get-workspace-settings.test.ts index f96d96738..a0a6b50e8 100644 --- a/apps/server/src/api/v2/security/get-workspace-settings.test.ts +++ b/apps/server/src/api/v2/security/get-workspace-settings.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getWorkspaceSettingsHandler } from './get-workspace-settings'; -import { createTestUser, createTestOrganization } from './test-fixtures'; import * as securityUtils from './security-utils'; +import { createTestOrganization, createTestUser } from './test-fixtures'; import { WorkspaceSettingsService } from './workspace-settings-service'; // Mock dependencies @@ -20,37 +20,46 @@ describe('getWorkspaceSettingsHandler', () => { defaultRole: 'restricted_querier', }); const mockOrgMembership = { organizationId: 'org-123', role: 'member' }; - + const mockDefaultDatasets = [ + { id: 'dataset-1', name: 'Sales Data' }, + { id: 'dataset-2', name: 'Customer Data' }, + ]; + beforeEach(() => { vi.clearAllMocks(); - + // Setup default mocks vi.mocked(securityUtils.validateUserOrganization).mockResolvedValue(mockOrgMembership); vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(mockOrg); - + vi.mocked(securityUtils.fetchDefaultDatasets).mockResolvedValue(mockDefaultDatasets); + // Setup workspace settings service mocks vi.mocked(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).mockReturnValue({ restrict_new_user_invitations: true, default_role: 'restricted_querier', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); }); it('should return settings for valid organization', async () => { const result = await getWorkspaceSettingsHandler(mockUser); - + expect(securityUtils.validateUserOrganization).toHaveBeenCalledWith(mockUser.id); expect(securityUtils.fetchOrganization).toHaveBeenCalledWith('org-123'); - - expect(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).toHaveBeenCalledWith({ - restrictNewUserInvitations: true, - defaultRole: 'restricted_querier', - }); - + expect(securityUtils.fetchDefaultDatasets).toHaveBeenCalledWith('org-123'); + + expect(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).toHaveBeenCalledWith( + { + restrictNewUserInvitations: true, + defaultRole: 'restricted_querier', + defaultDatasets: mockDefaultDatasets, + } + ); + expect(result).toEqual({ restrict_new_user_invitations: true, default_role: 'restricted_querier', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); }); @@ -61,24 +70,27 @@ describe('getWorkspaceSettingsHandler', () => { defaultRole: 'data_admin', }; vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(orgWithDifferentSettings); - + vi.mocked(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).mockReturnValue({ restrict_new_user_invitations: false, default_role: 'data_admin', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); - + const result = await getWorkspaceSettingsHandler(mockUser); - - expect(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).toHaveBeenCalledWith({ - restrictNewUserInvitations: false, - defaultRole: 'data_admin', - }); - + + expect(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).toHaveBeenCalledWith( + { + restrictNewUserInvitations: false, + defaultRole: 'data_admin', + defaultDatasets: mockDefaultDatasets, + } + ); + expect(result).toEqual({ restrict_new_user_invitations: false, default_role: 'data_admin', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); }); @@ -86,9 +98,9 @@ describe('getWorkspaceSettingsHandler', () => { vi.mocked(securityUtils.validateUserOrganization).mockRejectedValue( new Error('User not in organization') ); - + await expect(getWorkspaceSettingsHandler(mockUser)).rejects.toThrow( - 'User not in organization' + 'Failed to fetch workspace settings' ); }); @@ -96,36 +108,64 @@ describe('getWorkspaceSettingsHandler', () => { vi.mocked(securityUtils.fetchOrganization).mockRejectedValue( new Error('Organization not found') ); - + await expect(getWorkspaceSettingsHandler(mockUser)).rejects.toThrow( - 'Organization not found' + 'Failed to fetch workspace settings' ); }); it('should not require admin permissions', async () => { // Test with various non-admin roles const roles = ['querier', 'restricted_querier', 'viewer']; - + for (const role of roles) { vi.clearAllMocks(); const membership = { organizationId: 'org-123', role }; vi.mocked(securityUtils.validateUserOrganization).mockResolvedValue(membership); vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(mockOrg); - - vi.mocked(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).mockReturnValue({ - restrict_new_user_invitations: true, - default_role: 'restricted_querier', - default_datasets: [], - }); - + + vi.mocked(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).mockReturnValue( + { + restrict_new_user_invitations: true, + default_role: 'restricted_querier', + default_datasets: mockDefaultDatasets, + } + ); + const result = await getWorkspaceSettingsHandler(mockUser); - + // Should still succeed without admin permissions expect(result).toEqual({ restrict_new_user_invitations: true, default_role: 'restricted_querier', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); } }); -}); \ No newline at end of file + + it('should handle empty default datasets', async () => { + vi.mocked(securityUtils.fetchDefaultDatasets).mockResolvedValue([]); + + vi.mocked(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).mockReturnValue({ + restrict_new_user_invitations: true, + default_role: 'restricted_querier', + default_datasets: [], + }); + + const result = await getWorkspaceSettingsHandler(mockUser); + + expect(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).toHaveBeenCalledWith( + { + restrictNewUserInvitations: true, + defaultRole: 'restricted_querier', + defaultDatasets: [], + } + ); + + expect(result).toEqual({ + restrict_new_user_invitations: true, + default_role: 'restricted_querier', + default_datasets: [], + }); + }); +}); diff --git a/apps/server/src/api/v2/security/get-workspace-settings.ts b/apps/server/src/api/v2/security/get-workspace-settings.ts index 289ea50ad..dd3443caa 100644 --- a/apps/server/src/api/v2/security/get-workspace-settings.ts +++ b/apps/server/src/api/v2/security/get-workspace-settings.ts @@ -1,6 +1,11 @@ +import type { User } from '@buster/database'; import type { GetWorkspaceSettingsResponse } from '@buster/server-shared/security'; -import { type User } from '@buster/database'; -import { validateUserOrganization, fetchOrganization } from './security-utils'; +import { HTTPException } from 'hono/http-exception'; +import { + fetchDefaultDatasets, + fetchOrganization, + validateUserOrganization, +} from './security-utils'; import { WorkspaceSettingsService } from './workspace-settings-service'; const settingsService = new WorkspaceSettingsService(); @@ -8,15 +13,35 @@ const settingsService = new WorkspaceSettingsService(); export async function getWorkspaceSettingsHandler( user: User ): Promise { - // Validate user organization - const userOrg = await validateUserOrganization(user.id); + try { + // Validate user organization + const userOrg = await validateUserOrganization(user.id); - // Fetch organization - const org = await fetchOrganization(userOrg.organizationId); + // Fetch organization and default datasets concurrently + const [org, defaultDatasets] = await Promise.all([ + fetchOrganization(userOrg.organizationId), + fetchDefaultDatasets(userOrg.organizationId), + ]); - // Return formatted settings response - return settingsService.formatWorkspaceSettingsResponse({ - restrictNewUserInvitations: org.restrictNewUserInvitations, - defaultRole: org.defaultRole, - }); -} \ No newline at end of file + // Return formatted settings response + return settingsService.formatWorkspaceSettingsResponse({ + restrictNewUserInvitations: org.restrictNewUserInvitations, + defaultRole: org.defaultRole, + defaultDatasets, + }); + } catch (error) { + console.error('Error in getWorkspaceSettingsHandler:', { + userId: user.id, + error: error instanceof Error ? error.message : error, + }); + + // Re-throw HTTPException as is, wrap other errors + if (error instanceof HTTPException) { + throw error; + } + + throw new HTTPException(500, { + message: 'Failed to fetch workspace settings', + }); + } +} diff --git a/apps/server/src/api/v2/security/remove-approved-domains.int.test.ts b/apps/server/src/api/v2/security/remove-approved-domains.int.test.ts index 288f17b43..6fba2cfb9 100644 --- a/apps/server/src/api/v2/security/remove-approved-domains.int.test.ts +++ b/apps/server/src/api/v2/security/remove-approved-domains.int.test.ts @@ -1,15 +1,15 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import type { Organization, User } from '@buster/database'; +import { HTTPException } from 'hono/http-exception'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { removeApprovedDomainsHandler } from './remove-approved-domains'; import { - createTestUserInDb, - createTestOrganizationInDb, - createTestOrgMemberInDb, - cleanupTestUser, cleanupTestOrganization, + cleanupTestUser, + createTestOrgMemberInDb, + createTestOrganizationInDb, + createTestUserInDb, getOrganizationFromDb, } from './test-db-utils'; -import { HTTPException } from 'hono/http-exception'; -import type { User, Organization } from '@buster/database'; describe('removeApprovedDomainsHandler (integration)', () => { let testUser: User; @@ -37,8 +37,8 @@ describe('removeApprovedDomainsHandler (integration)', () => { const result = await removeApprovedDomainsHandler(request, testUser); expect(result).toHaveLength(2); - expect(result.map(d => d.domain)).toContain('keep.com'); - expect(result.map(d => d.domain)).toContain('stay.com'); + expect(result.map((d) => d.domain)).toContain('keep.com'); + expect(result.map((d) => d.domain)).toContain('stay.com'); // Verify database was updated const updatedOrg = await getOrganizationFromDb(testOrg.id); @@ -52,7 +52,7 @@ describe('removeApprovedDomainsHandler (integration)', () => { const result = await removeApprovedDomainsHandler(request, testUser); expect(result).toHaveLength(4); // All original domains remain - const domains = result.map(d => d.domain); + const domains = result.map((d) => d.domain); expect(domains).toContain('remove1.com'); expect(domains).toContain('remove2.com'); expect(domains).toContain('keep.com'); @@ -69,7 +69,7 @@ describe('removeApprovedDomainsHandler (integration)', () => { const result = await removeApprovedDomainsHandler(request, testUser); expect(result).toHaveLength(3); - const domains = result.map(d => d.domain); + const domains = result.map((d) => d.domain); expect(domains).not.toContain('remove1.com'); expect(domains).toContain('remove2.com'); expect(domains).toContain('keep.com'); @@ -83,7 +83,7 @@ describe('removeApprovedDomainsHandler (integration)', () => { const result = await removeApprovedDomainsHandler(request, testUser); expect(result).toHaveLength(2); - const domains = result.map(d => d.domain); + const domains = result.map((d) => d.domain); expect(domains).toEqual(['keep.com', 'stay.com']); }); @@ -94,7 +94,7 @@ describe('removeApprovedDomainsHandler (integration)', () => { const result = await removeApprovedDomainsHandler(request, testUser); expect(result).toHaveLength(2); - const domains = result.map(d => d.domain); + const domains = result.map((d) => d.domain); expect(domains).toEqual(['keep.com', 'stay.com']); }); }); @@ -161,7 +161,7 @@ describe('removeApprovedDomainsHandler (integration)', () => { await createTestOrgMemberInDb(testUser.id, testOrg.id, 'workspace_admin'); const originalUpdatedAt = testOrg.updatedAt; - await new Promise(resolve => setTimeout(resolve, 10)); // Ensure time difference + await new Promise((resolve) => setTimeout(resolve, 10)); // Ensure time difference const request = { domains: ['remove1.com'] }; await removeApprovedDomainsHandler(request, testUser); @@ -190,4 +190,4 @@ describe('removeApprovedDomainsHandler (integration)', () => { await cleanupTestOrganization(orgWithFewDomains.id); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/remove-approved-domains.test.ts b/apps/server/src/api/v2/security/remove-approved-domains.test.ts index 58e3f4577..2bba2fbb3 100644 --- a/apps/server/src/api/v2/security/remove-approved-domains.test.ts +++ b/apps/server/src/api/v2/security/remove-approved-domains.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { removeApprovedDomainsHandler } from './remove-approved-domains'; -import { createTestUser, createTestOrganization } from './test-fixtures'; -import * as securityUtils from './security-utils'; -import { DomainService } from './domain-service'; import { db } from '@buster/database'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { DomainService } from './domain-service'; +import { removeApprovedDomainsHandler } from './remove-approved-domains'; +import * as securityUtils from './security-utils'; +import { createTestOrganization, createTestUser } from './test-fixtures'; // Mock dependencies vi.mock('./security-utils'); @@ -32,21 +32,21 @@ describe('removeApprovedDomainsHandler', () => { domains: ['example.com', 'test.io', 'keep.com'], }); const mockOrgMembership = { organizationId: 'org-123', role: 'workspace_admin' }; - + beforeEach(() => { vi.clearAllMocks(); - + // Setup default mocks vi.mocked(securityUtils.validateUserOrganization).mockResolvedValue(mockOrgMembership); vi.mocked(securityUtils.checkAdminPermissions).mockImplementation(() => {}); vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(mockOrg); - + // Setup domain service mocks vi.mocked(DomainService.prototype.filterDomains).mockReturnValue(['keep.com']); vi.mocked(DomainService.prototype.formatDomainsResponse).mockReturnValue([ { domain: 'keep.com', created_at: mockOrg.createdAt }, ]); - + // Mock database update const mockDbChain = { update: vi.fn().mockReturnThis(), @@ -58,13 +58,13 @@ describe('removeApprovedDomainsHandler', () => { it('should remove specified domains', async () => { const request = { domains: ['example.com', 'test.io'] }; - + const result = await removeApprovedDomainsHandler(request, mockUser); - + expect(securityUtils.validateUserOrganization).toHaveBeenCalledWith(mockUser.id); expect(securityUtils.checkAdminPermissions).toHaveBeenCalledWith('workspace_admin'); expect(securityUtils.fetchOrganization).toHaveBeenCalledWith('org-123'); - + expect(DomainService.prototype.filterDomains).toHaveBeenCalledWith( ['example.com', 'test.io', 'keep.com'], ['example.com', 'test.io'] @@ -73,23 +73,25 @@ describe('removeApprovedDomainsHandler', () => { ['keep.com'], mockOrg.createdAt ); - - expect(result).toEqual([ - { domain: 'keep.com', created_at: mockOrg.createdAt }, - ]); + + expect(result).toEqual([{ domain: 'keep.com', created_at: mockOrg.createdAt }]); }); it('should handle non-existent domain removal gracefully', async () => { - vi.mocked(DomainService.prototype.filterDomains).mockReturnValue(['example.com', 'test.io', 'keep.com']); + vi.mocked(DomainService.prototype.filterDomains).mockReturnValue([ + 'example.com', + 'test.io', + 'keep.com', + ]); vi.mocked(DomainService.prototype.formatDomainsResponse).mockReturnValue([ { domain: 'example.com', created_at: mockOrg.createdAt }, { domain: 'test.io', created_at: mockOrg.createdAt }, { domain: 'keep.com', created_at: mockOrg.createdAt }, ]); - + const request = { domains: ['notfound.com'] }; const result = await removeApprovedDomainsHandler(request, mockUser); - + expect(DomainService.prototype.filterDomains).toHaveBeenCalledWith( ['example.com', 'test.io', 'keep.com'], ['notfound.com'] @@ -105,9 +107,9 @@ describe('removeApprovedDomainsHandler', () => { where: vi.fn().mockResolvedValue(undefined), }; vi.mocked(db.update).mockReturnValue(mockDbChain as any); - + await removeApprovedDomainsHandler(request, mockUser); - + expect(db.update).toHaveBeenCalled(); expect(mockDbChain.set).toHaveBeenCalledWith({ domains: ['keep.com'], @@ -118,13 +120,13 @@ describe('removeApprovedDomainsHandler', () => { it('should handle empty domains array', async () => { const orgWithNoDomains = { ...mockOrg, domains: null }; vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(orgWithNoDomains); - + vi.mocked(DomainService.prototype.filterDomains).mockReturnValue([]); vi.mocked(DomainService.prototype.formatDomainsResponse).mockReturnValue([]); - + const request = { domains: ['example.com'] }; const result = await removeApprovedDomainsHandler(request, mockUser); - + expect(DomainService.prototype.filterDomains).toHaveBeenCalledWith([], ['example.com']); expect(result).toEqual([]); }); @@ -133,9 +135,9 @@ describe('removeApprovedDomainsHandler', () => { vi.mocked(securityUtils.validateUserOrganization).mockRejectedValue( new Error('User not in organization') ); - + const request = { domains: ['example.com'] }; - + await expect(removeApprovedDomainsHandler(request, mockUser)).rejects.toThrow( 'User not in organization' ); @@ -145,11 +147,11 @@ describe('removeApprovedDomainsHandler', () => { vi.mocked(securityUtils.checkAdminPermissions).mockImplementation(() => { throw new Error('Insufficient permissions'); }); - + const request = { domains: ['example.com'] }; - + await expect(removeApprovedDomainsHandler(request, mockUser)).rejects.toThrow( 'Insufficient permissions' ); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/security-utils.test.ts b/apps/server/src/api/v2/security/security-utils.test.ts index 1d429657d..74b1b0111 100644 --- a/apps/server/src/api/v2/security/security-utils.test.ts +++ b/apps/server/src/api/v2/security/security-utils.test.ts @@ -1,10 +1,13 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; import { HTTPException } from 'hono/http-exception'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { - validateUserOrganization, - fetchOrganization, checkAdminPermissions, checkWorkspaceAdminPermission, + ensureDefaultPermissionGroup, + fetchDefaultDatasets, + fetchOrganization, + updateDefaultDatasets, + validateUserOrganization, } from './security-utils'; import { createTestOrganization } from './test-fixtures'; @@ -16,11 +19,20 @@ vi.mock('@buster/database', () => ({ from: vi.fn(), where: vi.fn(), limit: vi.fn(), + insert: vi.fn(), + update: vi.fn(), + transaction: vi.fn(), }, organizations: {}, + datasets: {}, + datasetsToPermissionGroups: {}, + permissionGroups: {}, + eq: vi.fn(), + and: vi.fn(), + isNull: vi.fn(), })); -import { getUserOrganizationId, db } from '@buster/database'; +import { and, db, eq, getUserOrganizationId, isNull } from '@buster/database'; describe('security-utils', () => { beforeEach(() => { @@ -110,16 +122,20 @@ describe('security-utils', () => { it('should reject other roles', () => { const roles = ['member', 'viewer', 'user', 'guest']; - - roles.forEach(role => { + + roles.forEach((role) => { expect(() => checkAdminPermissions(role)).toThrow(HTTPException); - expect(() => checkAdminPermissions(role)).toThrow('Insufficient permissions to manage approved domains'); + expect(() => checkAdminPermissions(role)).toThrow( + 'Insufficient permissions to manage approved domains' + ); }); }); it('should reject null role', () => { expect(() => checkAdminPermissions(null)).toThrow(HTTPException); - expect(() => checkAdminPermissions(null)).toThrow('Insufficient permissions to manage approved domains'); + expect(() => checkAdminPermissions(null)).toThrow( + 'Insufficient permissions to manage approved domains' + ); }); it('should reject undefined role', () => { @@ -134,21 +150,414 @@ describe('security-utils', () => { it('should reject data_admin role', () => { expect(() => checkWorkspaceAdminPermission('data_admin')).toThrow(HTTPException); - expect(() => checkWorkspaceAdminPermission('data_admin')).toThrow('Only workspace admins can update workspace settings'); + expect(() => checkWorkspaceAdminPermission('data_admin')).toThrow( + 'Only workspace admins can update workspace settings' + ); }); it('should reject other roles', () => { const roles = ['admin', 'member', 'viewer', 'user']; - - roles.forEach(role => { + + roles.forEach((role) => { expect(() => checkWorkspaceAdminPermission(role)).toThrow(HTTPException); - expect(() => checkWorkspaceAdminPermission(role)).toThrow('Only workspace admins can update workspace settings'); + expect(() => checkWorkspaceAdminPermission(role)).toThrow( + 'Only workspace admins can update workspace settings' + ); }); }); it('should reject null role', () => { expect(() => checkWorkspaceAdminPermission(null)).toThrow(HTTPException); - expect(() => checkWorkspaceAdminPermission(null)).toThrow('Only workspace admins can update workspace settings'); + expect(() => checkWorkspaceAdminPermission(null)).toThrow( + 'Only workspace admins can update workspace settings' + ); }); }); -}); \ No newline at end of file + + describe('fetchDefaultDatasets', () => { + it('should return datasets from default permission group', async () => { + const mockDatasets = [ + { id: 'dataset-1', name: 'Sales Data' }, + { id: 'dataset-2', name: 'Customer Data' }, + ]; + + const mockDbChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + innerJoin: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(mockDatasets), + }; + + vi.mocked(db.select).mockReturnValue(mockDbChain as any); + + const result = await fetchDefaultDatasets('org-123'); + + expect(result).toEqual(mockDatasets); + expect(mockDbChain.innerJoin).toHaveBeenCalledTimes(2); + }); + + it('should return empty array when no default datasets', async () => { + const mockDbChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + innerJoin: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue([]), + }; + + vi.mocked(db.select).mockReturnValue(mockDbChain as any); + + const result = await fetchDefaultDatasets('org-123'); + + expect(result).toEqual([]); + }); + }); + + describe('ensureDefaultPermissionGroup', () => { + it('should return existing permission group ID', async () => { + const mockExistingGroup = [{ id: 'pg-123', name: 'default:org-123' }]; + + const mockDbChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue(mockExistingGroup), + }; + + vi.mocked(db.select).mockReturnValue(mockDbChain as any); + + const result = await ensureDefaultPermissionGroup('org-123', 'user-123'); + + expect(result).toBe('pg-123'); + expect(db.insert).not.toHaveBeenCalled(); + }); + + it('should create new permission group when not exists', async () => { + const mockDbSelectChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([]), + }; + + const mockDbInsertChain = { + insert: vi.fn().mockReturnThis(), + values: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([{ id: 'pg-new' }]), + }; + + vi.mocked(db.select).mockReturnValue(mockDbSelectChain as any); + vi.mocked(db.insert).mockReturnValue(mockDbInsertChain as any); + + const result = await ensureDefaultPermissionGroup('org-123', 'user-123'); + + expect(result).toBe('pg-new'); + expect(mockDbInsertChain.values).toHaveBeenCalledWith({ + name: 'default:org-123', + organizationId: 'org-123', + createdBy: 'user-123', + updatedBy: 'user-123', + }); + }); + + it('should restore soft deleted permission group', async () => { + const mockExistingGroup = [ + { + id: 'pg-123', + name: 'default:org-123', + deletedAt: '2024-01-01T00:00:00Z', + }, + ]; + + const mockDbSelectChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue(mockExistingGroup), + }; + + const mockDbUpdateChain = { + update: vi.fn().mockReturnThis(), + set: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(undefined), + }; + + vi.mocked(db.select).mockReturnValue(mockDbSelectChain as any); + vi.mocked(db.update).mockReturnValue(mockDbUpdateChain as any); + + const result = await ensureDefaultPermissionGroup('org-123', 'user-123'); + + expect(result).toBe('pg-123'); + expect(db.update).toHaveBeenCalled(); + expect(mockDbUpdateChain.set).toHaveBeenCalledWith({ + deletedAt: null, + updatedBy: 'user-123', + updatedAt: expect.any(String), + }); + }); + + it('should throw error when insert fails', async () => { + const mockDbSelectChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([]), + }; + + const mockDbInsertChain = { + insert: vi.fn().mockReturnThis(), + values: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([]), + }; + + vi.mocked(db.select).mockReturnValue(mockDbSelectChain as any); + vi.mocked(db.insert).mockReturnValue(mockDbInsertChain as any); + + await expect(ensureDefaultPermissionGroup('org-123', 'user-123')).rejects.toThrow( + HTTPException + ); + await expect(ensureDefaultPermissionGroup('org-123', 'user-123')).rejects.toMatchObject({ + status: 500, + message: 'Failed to create default permission group', + }); + }); + }); + + describe('updateDefaultDatasets', () => { + it('should update datasets with specific IDs', async () => { + // Mock ensureDefaultPermissionGroup + const mockDbSelectChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([{ id: 'pg-123' }]), + }; + + // Mock valid datasets query + const mockValidDatasets = [{ id: 'dataset-1' }, { id: 'dataset-2' }]; + + vi.mocked(db.select) + .mockReturnValueOnce(mockDbSelectChain as any) // For permission group + .mockReturnValueOnce({ + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(mockValidDatasets), + } as any); // For dataset validation + + // Mock transaction + const mockTx = { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([]), // No existing records + }), + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(undefined), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockReturnThis(), + onConflictDoUpdate: vi.fn().mockResolvedValue(undefined), + }), + }; + + vi.mocked(db.transaction).mockImplementation(async (callback) => { + return callback(mockTx as any); + }); + + await updateDefaultDatasets('org-123', ['dataset-1', 'dataset-2'], 'user-123'); + + expect(mockTx.update).toHaveBeenCalled(); + expect(mockTx.insert).toHaveBeenCalled(); + }); + + it('should update datasets with "all" keyword', async () => { + // Mock ensureDefaultPermissionGroup + const mockDbSelectChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([{ id: 'pg-123' }]), + }; + + // Mock fetching all datasets + const mockAllDatasets = [{ id: 'dataset-1' }, { id: 'dataset-2' }, { id: 'dataset-3' }]; + + vi.mocked(db.select) + .mockReturnValueOnce(mockDbSelectChain as any) // For permission group + .mockReturnValueOnce({ + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(mockAllDatasets), + } as any); // For all datasets + + // Mock transaction + const mockTx = { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([]), // No existing records + }), + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(undefined), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockReturnThis(), + onConflictDoUpdate: vi.fn().mockResolvedValue(undefined), + }), + }; + + vi.mocked(db.transaction).mockImplementation(async (callback) => { + return callback(mockTx as any); + }); + + await updateDefaultDatasets('org-123', 'all', 'user-123'); + + expect(mockTx.insert).toHaveBeenCalled(); + }); + + it('should handle empty dataset array', async () => { + // Mock ensureDefaultPermissionGroup + const mockDbSelectChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([{ id: 'pg-123' }]), + }; + + // Mock empty dataset validation query + vi.mocked(db.select) + .mockReturnValueOnce(mockDbSelectChain as any) // For permission group + .mockReturnValueOnce({ + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue([]), + } as any); // For dataset validation (empty) + + // Mock transaction + const mockTx = { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([]), // No existing records + }), + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(undefined), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockReturnThis(), + onConflictDoUpdate: vi.fn().mockResolvedValue(undefined), + }), + }; + + vi.mocked(db.transaction).mockImplementation(async (callback) => { + return callback(mockTx as any); + }); + + await updateDefaultDatasets('org-123', [], 'user-123'); + + expect(mockTx.update).toHaveBeenCalled(); + expect(mockTx.insert).not.toHaveBeenCalled(); + }); + + it('should filter out invalid dataset IDs', async () => { + // Mock ensureDefaultPermissionGroup + const mockDbSelectChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([{ id: 'pg-123' }]), + }; + + // Mock valid datasets query + const mockValidDatasets = [{ id: 'dataset-1' }, { id: 'dataset-2' }]; + + vi.mocked(db.select) + .mockReturnValueOnce(mockDbSelectChain as any) // For permission group + .mockReturnValueOnce({ + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(mockValidDatasets), + } as any); // For dataset validation + + // Mock transaction + const mockTx = { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([]), // No existing records + }), + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(undefined), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockReturnThis(), + onConflictDoUpdate: vi.fn().mockResolvedValue(undefined), + }), + }; + + vi.mocked(db.transaction).mockImplementation(async (callback) => { + return callback(mockTx as any); + }); + + // Try to update with some invalid dataset IDs + await updateDefaultDatasets('org-123', ['dataset-1', 'dataset-2', 'invalid-id'], 'user-123'); + + // Should only insert valid datasets + expect(mockTx.insert).toHaveBeenCalled(); + // With the new batch upsert logic, insert is called once with all valid datasets + expect(mockTx.insert).toHaveBeenCalledTimes(1); + }); + + it('should restore soft-deleted dataset associations', async () => { + // Mock ensureDefaultPermissionGroup + const mockDbSelectChain = { + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + limit: vi.fn().mockResolvedValue([{ id: 'pg-123' }]), + }; + + // Mock valid datasets query + const mockValidDatasets = [{ id: 'dataset-1' }, { id: 'dataset-2' }]; + + vi.mocked(db.select) + .mockReturnValueOnce(mockDbSelectChain as any) // For permission group + .mockReturnValueOnce({ + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(mockValidDatasets), + } as any); // For dataset validation + + // Mock transaction + const mockTx = { + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(undefined), + }), + insert: vi.fn().mockReturnValue({ + values: vi.fn().mockReturnThis(), + onConflictDoUpdate: vi.fn().mockResolvedValue(undefined), + }), + }; + + vi.mocked(db.transaction).mockImplementation(async (callback) => { + return callback(mockTx as any); + }); + + await updateDefaultDatasets('org-123', ['dataset-1', 'dataset-2'], 'user-123'); + + // Should soft-delete all existing first, then batch upsert + expect(mockTx.update).toHaveBeenCalledTimes(1); // 1 for soft-delete all + expect(mockTx.insert).toHaveBeenCalledTimes(1); // 1 batch upsert for both datasets + + // Verify the insert was called with values + expect(mockTx.insert).toHaveBeenCalled(); + const insertMock = vi.mocked(mockTx.insert); + expect(insertMock().values).toHaveBeenCalled(); + expect(insertMock().values().onConflictDoUpdate).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/server/src/api/v2/security/security-utils.ts b/apps/server/src/api/v2/security/security-utils.ts index 16cd83541..32eecde23 100644 --- a/apps/server/src/api/v2/security/security-utils.ts +++ b/apps/server/src/api/v2/security/security-utils.ts @@ -1,6 +1,13 @@ -import { HTTPException } from 'hono/http-exception'; -import { db, getUserOrganizationId, organizations } from '@buster/database'; +import { + datasets, + datasetsToPermissionGroups, + db, + getUserOrganizationId, + organizations, + permissionGroups, +} from '@buster/database'; import { and, eq, isNull } from 'drizzle-orm'; +import { HTTPException } from 'hono/http-exception'; export async function validateUserOrganization(userId: string) { const userOrg = await getUserOrganizationId(userId); @@ -16,12 +23,7 @@ export async function fetchOrganization(organizationId: string) { const org = await db .select() .from(organizations) - .where( - and( - eq(organizations.id, organizationId), - isNull(organizations.deletedAt) - ) - ) + .where(and(eq(organizations.id, organizationId), isNull(organizations.deletedAt))) .limit(1); if (!org.length || !org[0]) { @@ -29,7 +31,7 @@ export async function fetchOrganization(organizationId: string) { message: 'Organization not found', }); } - + return org[0]; } @@ -47,4 +49,280 @@ export function checkWorkspaceAdminPermission(role: string | null): void { message: 'Only workspace admins can update workspace settings', }); } -} \ No newline at end of file +} + +export async function fetchDefaultDatasets(organizationId: string) { + const defaultPermissionGroupName = `default:${organizationId}`; + + try { + const defaultDatasets = await db + .select({ + id: datasets.id, + name: datasets.name, + }) + .from(datasets) + .innerJoin( + datasetsToPermissionGroups, + and( + eq(datasets.id, datasetsToPermissionGroups.datasetId), + isNull(datasetsToPermissionGroups.deletedAt) + ) + ) + .innerJoin( + permissionGroups, + and( + eq(datasetsToPermissionGroups.permissionGroupId, permissionGroups.id), + eq(permissionGroups.name, defaultPermissionGroupName), + eq(permissionGroups.organizationId, organizationId), + isNull(permissionGroups.deletedAt) + ) + ) + .where(and(eq(datasets.organizationId, organizationId), isNull(datasets.deletedAt))); + + return defaultDatasets; + } catch (error) { + console.error('Error fetching default datasets:', { + organizationId, + permissionGroupName: defaultPermissionGroupName, + error: error instanceof Error ? error.message : error, + }); + throw new HTTPException(500, { + message: 'Failed to fetch default datasets', + }); + } +} + +export async function ensureDefaultPermissionGroup(organizationId: string, userId: string) { + const defaultPermissionGroupName = `default:${organizationId}`; + + try { + // Check if it exists (including soft deleted ones) + const existingGroup = await db + .select() + .from(permissionGroups) + .where( + and( + eq(permissionGroups.name, defaultPermissionGroupName), + eq(permissionGroups.organizationId, organizationId) + ) + ) + .limit(1); + + if (existingGroup.length > 0 && existingGroup[0]) { + // If it's soft deleted, restore it + if (existingGroup[0].deletedAt) { + try { + await db + .update(permissionGroups) + .set({ + deletedAt: null, + updatedBy: userId, + updatedAt: new Date().toISOString(), + }) + .where(eq(permissionGroups.id, existingGroup[0].id)); + + console.info('Restored soft-deleted default permission group:', { + permissionGroupId: existingGroup[0].id, + organizationId, + }); + } catch (error) { + console.error('Error restoring default permission group:', { + permissionGroupId: existingGroup[0].id, + organizationId, + error: error instanceof Error ? error.message : error, + }); + throw new HTTPException(500, { + message: 'Failed to restore default permission group', + }); + } + } + return existingGroup[0].id; + } + + // Create if doesn't exist + const newGroup = await db + .insert(permissionGroups) + .values({ + name: defaultPermissionGroupName, + organizationId, + createdBy: userId, + updatedBy: userId, + }) + .returning(); + + if (!newGroup[0]) { + console.error('Failed to create default permission group - no data returned:', { + organizationId, + userId, + permissionGroupName: defaultPermissionGroupName, + }); + throw new HTTPException(500, { + message: 'Failed to create default permission group', + }); + } + + console.info('Created new default permission group:', { + permissionGroupId: newGroup[0].id, + organizationId, + }); + + return newGroup[0].id; + } catch (error) { + // If it's already an HTTPException, re-throw it + if (error instanceof HTTPException) { + throw error; + } + + console.error('Error ensuring default permission group:', { + organizationId, + userId, + permissionGroupName: defaultPermissionGroupName, + error: error instanceof Error ? error.message : error, + }); + throw new HTTPException(500, { + message: 'Failed to ensure default permission group exists', + }); + } +} + +export async function updateDefaultDatasets( + organizationId: string, + datasetIds: string[] | 'all', + userId: string +) { + try { + const defaultPermissionGroupId = await ensureDefaultPermissionGroup(organizationId, userId); + + // If 'all', fetch all organization datasets + let finalDatasetIds = datasetIds; + if (datasetIds === 'all') { + try { + const allDatasets = await db + .select({ id: datasets.id }) + .from(datasets) + .where(and(eq(datasets.organizationId, organizationId), isNull(datasets.deletedAt))); + finalDatasetIds = allDatasets.map((d) => d.id); + + console.info('Fetched all organization datasets for default assignment:', { + organizationId, + datasetCount: finalDatasetIds.length, + }); + } catch (error) { + console.error('Error fetching all organization datasets:', { + organizationId, + error: error instanceof Error ? error.message : error, + }); + throw new HTTPException(500, { + message: 'Failed to fetch organization datasets', + }); + } + } else { + // Validate that the provided dataset IDs exist and aren't deleted + try { + const validDatasets = await db + .select({ id: datasets.id }) + .from(datasets) + .where(and(eq(datasets.organizationId, organizationId), isNull(datasets.deletedAt))); + const validDatasetIds = new Set(validDatasets.map((d) => d.id)); + const invalidIds = datasetIds.filter((id) => !validDatasetIds.has(id)); + finalDatasetIds = datasetIds.filter((id) => validDatasetIds.has(id)); + + if (invalidIds.length > 0) { + console.warn('Some dataset IDs were invalid or deleted:', { + organizationId, + invalidIds, + requestedIds: datasetIds, + validIds: finalDatasetIds, + }); + } + } catch (error) { + console.error('Error validating dataset IDs:', { + organizationId, + datasetIds, + error: error instanceof Error ? error.message : error, + }); + throw new HTTPException(500, { + message: 'Failed to validate dataset IDs', + }); + } + } + + // Start a transaction to update datasets atomically + await db.transaction(async (tx) => { + try { + // First, soft delete all existing associations for this permission group + await tx + .update(datasetsToPermissionGroups) + .set({ + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }) + .where( + and( + eq(datasetsToPermissionGroups.permissionGroupId, defaultPermissionGroupId), + isNull(datasetsToPermissionGroups.deletedAt) + ) + ); + + // Add or restore dataset associations using upserts + if (Array.isArray(finalDatasetIds) && finalDatasetIds.length > 0) { + const currentTime = new Date().toISOString(); + + // Prepare values for batch upsert + const values = finalDatasetIds.map((datasetId) => ({ + datasetId, + permissionGroupId: defaultPermissionGroupId, + createdAt: currentTime, + updatedAt: currentTime, + deletedAt: null, + })); + + // Perform upsert - if the combination of datasetId and permissionGroupId exists, + // update deletedAt to null and updatedAt to current time + await tx + .insert(datasetsToPermissionGroups) + .values(values) + .onConflictDoUpdate({ + target: [ + datasetsToPermissionGroups.datasetId, + datasetsToPermissionGroups.permissionGroupId, + ], + set: { + deletedAt: null, + updatedAt: currentTime, + }, + }); + } + + console.info('Successfully updated default datasets:', { + organizationId, + permissionGroupId: defaultPermissionGroupId, + datasetCount: Array.isArray(finalDatasetIds) ? finalDatasetIds.length : 0, + operation: datasetIds === 'all' ? 'all' : 'specific', + }); + } catch (error) { + console.error('Error in dataset update transaction:', { + organizationId, + permissionGroupId: defaultPermissionGroupId, + error: error instanceof Error ? error.message : error, + }); + throw error; // Re-throw to trigger transaction rollback + } + }); + } catch (error) { + // If it's already an HTTPException, re-throw it + if (error instanceof HTTPException) { + throw error; + } + + console.error('Error updating default datasets:', { + organizationId, + userId, + datasetIds, + error: error instanceof Error ? error.message : error, + }); + throw new HTTPException(500, { + message: 'Failed to update default datasets', + }); + } +} diff --git a/apps/server/src/api/v2/security/test-db-utils.ts b/apps/server/src/api/v2/security/test-db-utils.ts index f296b5582..96498f943 100644 --- a/apps/server/src/api/v2/security/test-db-utils.ts +++ b/apps/server/src/api/v2/security/test-db-utils.ts @@ -1,7 +1,7 @@ -import { db, organizations, usersToOrganizations, users } from '@buster/database'; -import { eq, and, isNull } from 'drizzle-orm'; -import type { User, Organization } from '@buster/database'; import { randomUUID } from 'crypto'; +import { db, organizations, users, usersToOrganizations } from '@buster/database'; +import type { Organization, User } from '@buster/database'; +import { and, eq, isNull } from 'drizzle-orm'; export async function createTestUserInDb(userData: Partial = {}): Promise { const id = randomUUID(); @@ -44,7 +44,7 @@ export async function createTestOrganizationInDb( export async function createTestOrgMemberInDb( userId: string, organizationId: string, - role: string = 'querier' + role = 'querier' ): Promise { const member = { userId, @@ -63,7 +63,7 @@ export async function createTestOrgMemberInDb( export async function cleanupTestUser(userId: string): Promise { // Delete organization memberships await db.delete(usersToOrganizations).where(eq(usersToOrganizations.userId, userId)); - + // Delete user await db.delete(users).where(eq(users.id, userId)); } @@ -71,7 +71,7 @@ export async function cleanupTestUser(userId: string): Promise { export async function cleanupTestOrganization(orgId: string): Promise { // Delete organization memberships await db.delete(usersToOrganizations).where(eq(usersToOrganizations.organizationId, orgId)); - + // Delete organization await db.delete(organizations).where(eq(organizations.id, orgId)); } @@ -80,13 +80,8 @@ export async function getOrganizationFromDb(orgId: string): Promise): User { const id = `test-user-${Math.random().toString(36).substring(7)}`; @@ -33,7 +33,7 @@ export function createTestOrganization(overrides?: Partial): Organ export function createTestOrgMember( userId: string, organizationId: string, - role: string = 'querier' + role = 'querier' ): { organizationId: string; role: string } { return { organizationId, @@ -71,4 +71,4 @@ export function createMockDb() { mockSet, mockReturning, }; -} \ No newline at end of file +} diff --git a/apps/server/src/api/v2/security/update-workspace-settings.int.test.ts b/apps/server/src/api/v2/security/update-workspace-settings.int.test.ts index 1dd60e9cd..e1320cda5 100644 --- a/apps/server/src/api/v2/security/update-workspace-settings.int.test.ts +++ b/apps/server/src/api/v2/security/update-workspace-settings.int.test.ts @@ -1,15 +1,15 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { updateWorkspaceSettingsHandler } from './update-workspace-settings'; +import type { Organization, User } from '@buster/database'; +import { HTTPException } from 'hono/http-exception'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { - createTestUserInDb, - createTestOrganizationInDb, - createTestOrgMemberInDb, - cleanupTestUser, cleanupTestOrganization, + cleanupTestUser, + createTestOrgMemberInDb, + createTestOrganizationInDb, + createTestUserInDb, getOrganizationFromDb, } from './test-db-utils'; -import { HTTPException } from 'hono/http-exception'; -import type { User, Organization } from '@buster/database'; +import { updateWorkspaceSettingsHandler } from './update-workspace-settings'; describe('updateWorkspaceSettingsHandler (integration)', () => { let testUser: User; @@ -121,7 +121,9 @@ describe('updateWorkspaceSettingsHandler (integration)', () => { const request = { restrict_new_user_invitations: true }; - await expect(updateWorkspaceSettingsHandler(request, roleUser)).rejects.toThrow(HTTPException); + await expect(updateWorkspaceSettingsHandler(request, roleUser)).rejects.toThrow( + HTTPException + ); await expect(updateWorkspaceSettingsHandler(request, roleUser)).rejects.toMatchObject({ status: 403, message: 'Only workspace admins can update workspace settings', @@ -144,7 +146,9 @@ describe('updateWorkspaceSettingsHandler (integration)', () => { const request = { restrict_new_user_invitations: true }; // When organization is deleted, user effectively has no organization - await expect(updateWorkspaceSettingsHandler(request, testUser)).rejects.toThrow(HTTPException); + await expect(updateWorkspaceSettingsHandler(request, testUser)).rejects.toThrow( + HTTPException + ); await expect(updateWorkspaceSettingsHandler(request, testUser)).rejects.toMatchObject({ status: 403, message: 'User is not associated with an organization', @@ -154,7 +158,9 @@ describe('updateWorkspaceSettingsHandler (integration)', () => { it('should return 403 for user without organization', async () => { const request = { restrict_new_user_invitations: true }; - await expect(updateWorkspaceSettingsHandler(request, testUser)).rejects.toThrow(HTTPException); + await expect(updateWorkspaceSettingsHandler(request, testUser)).rejects.toThrow( + HTTPException + ); await expect(updateWorkspaceSettingsHandler(request, testUser)).rejects.toMatchObject({ status: 403, message: 'User is not associated with an organization', @@ -197,7 +203,7 @@ describe('updateWorkspaceSettingsHandler (integration)', () => { await createTestOrgMemberInDb(testUser.id, testOrg.id, 'workspace_admin'); const originalUpdatedAt = testOrg.updatedAt; - await new Promise(resolve => setTimeout(resolve, 10)); // Ensure time difference + await new Promise((resolve) => setTimeout(resolve, 10)); // Ensure time difference const request = { restrict_new_user_invitations: true }; await updateWorkspaceSettingsHandler(request, testUser); @@ -229,4 +235,4 @@ describe('updateWorkspaceSettingsHandler (integration)', () => { ); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/update-workspace-settings.test.ts b/apps/server/src/api/v2/security/update-workspace-settings.test.ts index 3f88e8662..883566fd8 100644 --- a/apps/server/src/api/v2/security/update-workspace-settings.test.ts +++ b/apps/server/src/api/v2/security/update-workspace-settings.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { updateWorkspaceSettingsHandler } from './update-workspace-settings'; -import { createTestUser, createTestOrganization } from './test-fixtures'; -import * as securityUtils from './security-utils'; -import { WorkspaceSettingsService } from './workspace-settings-service'; import { db } from '@buster/database'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import * as securityUtils from './security-utils'; +import { createTestOrganization, createTestUser } from './test-fixtures'; +import { updateWorkspaceSettingsHandler } from './update-workspace-settings'; +import { WorkspaceSettingsService } from './workspace-settings-service'; // Mock dependencies vi.mock('./security-utils'); @@ -33,17 +33,21 @@ describe('updateWorkspaceSettingsHandler', () => { defaultRole: 'restricted_querier', }); const mockOrgMembership = { organizationId: 'org-123', role: 'workspace_admin' }; - + const mockDefaultDatasets = [ + { id: 'dataset-1', name: 'Sales Data' }, + { id: 'dataset-2', name: 'Customer Data' }, + ]; + beforeEach(() => { vi.clearAllMocks(); - + // Setup default mocks vi.mocked(securityUtils.validateUserOrganization).mockResolvedValue(mockOrgMembership); vi.mocked(securityUtils.checkWorkspaceAdminPermission).mockImplementation(() => {}); - vi.mocked(securityUtils.fetchOrganization) - .mockResolvedValueOnce(mockOrg) // First call for initial fetch - .mockResolvedValueOnce({ ...mockOrg, restrictNewUserInvitations: true }); // Second call after update - + vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(mockOrg); + vi.mocked(securityUtils.updateDefaultDatasets).mockResolvedValue(undefined); + vi.mocked(securityUtils.fetchDefaultDatasets).mockResolvedValue(mockDefaultDatasets); + // Setup workspace settings service mocks vi.mocked(WorkspaceSettingsService.prototype.buildUpdateData).mockReturnValue({ updatedAt: '2024-01-01T00:00:00Z', @@ -52,9 +56,9 @@ describe('updateWorkspaceSettingsHandler', () => { vi.mocked(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).mockReturnValue({ restrict_new_user_invitations: true, default_role: 'restricted_querier', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); - + // Mock database update const mockDbChain = { update: vi.fn().mockReturnThis(), @@ -69,7 +73,7 @@ describe('updateWorkspaceSettingsHandler', () => { restrict_new_user_invitations: true, default_role: 'data_admin', }; - + vi.mocked(WorkspaceSettingsService.prototype.buildUpdateData).mockReturnValue({ updatedAt: '2024-01-01T00:00:00Z', restrictNewUserInvitations: true, @@ -78,33 +82,33 @@ describe('updateWorkspaceSettingsHandler', () => { vi.mocked(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).mockReturnValue({ restrict_new_user_invitations: true, default_role: 'data_admin', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); - + const result = await updateWorkspaceSettingsHandler(request, mockUser); - + expect(securityUtils.validateUserOrganization).toHaveBeenCalledWith(mockUser.id); expect(securityUtils.checkWorkspaceAdminPermission).toHaveBeenCalledWith('workspace_admin'); expect(WorkspaceSettingsService.prototype.buildUpdateData).toHaveBeenCalledWith(request); - + expect(result).toEqual({ restrict_new_user_invitations: true, default_role: 'data_admin', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); }); it('should handle partial updates correctly', async () => { const request = { restrict_new_user_invitations: true }; - + const result = await updateWorkspaceSettingsHandler(request, mockUser); - + expect(WorkspaceSettingsService.prototype.buildUpdateData).toHaveBeenCalledWith(request); - + expect(result).toEqual({ restrict_new_user_invitations: true, default_role: 'restricted_querier', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); }); @@ -116,7 +120,7 @@ describe('updateWorkspaceSettingsHandler', () => { where: vi.fn().mockResolvedValue(undefined), }; vi.mocked(db.update).mockReturnValue(mockDbChain as any); - + vi.mocked(WorkspaceSettingsService.prototype.buildUpdateData).mockReturnValue({ updatedAt: '2024-01-01T00:00:00Z', defaultRole: 'data_admin', @@ -124,11 +128,11 @@ describe('updateWorkspaceSettingsHandler', () => { vi.mocked(WorkspaceSettingsService.prototype.formatWorkspaceSettingsResponse).mockReturnValue({ restrict_new_user_invitations: false, default_role: 'data_admin', - default_datasets: [], + default_datasets: mockDefaultDatasets, }); - + await updateWorkspaceSettingsHandler(request, mockUser); - + expect(db.update).toHaveBeenCalled(); expect(mockDbChain.set).toHaveBeenCalledWith({ updatedAt: '2024-01-01T00:00:00Z', @@ -138,9 +142,9 @@ describe('updateWorkspaceSettingsHandler', () => { it('should fetch updated organization after update', async () => { const request = { restrict_new_user_invitations: true }; - + await updateWorkspaceSettingsHandler(request, mockUser); - + expect(securityUtils.fetchOrganization).toHaveBeenCalledTimes(1); expect(securityUtils.fetchOrganization).toHaveBeenCalledWith('org-123'); }); @@ -149,11 +153,11 @@ describe('updateWorkspaceSettingsHandler', () => { vi.mocked(securityUtils.validateUserOrganization).mockRejectedValue( new Error('User not in organization') ); - + const request = { restrict_new_user_invitations: true }; - + await expect(updateWorkspaceSettingsHandler(request, mockUser)).rejects.toThrow( - 'User not in organization' + 'Failed to update workspace settings' ); }); @@ -161,11 +165,11 @@ describe('updateWorkspaceSettingsHandler', () => { vi.mocked(securityUtils.checkWorkspaceAdminPermission).mockImplementation(() => { throw new Error('Only workspace admins can update settings'); }); - + const request = { restrict_new_user_invitations: true }; - + await expect(updateWorkspaceSettingsHandler(request, mockUser)).rejects.toThrow( - 'Only workspace admins can update settings' + 'Failed to update workspace settings' ); }); @@ -177,11 +181,58 @@ describe('updateWorkspaceSettingsHandler', () => { throw new Error('Only workspace admins can update settings'); } }); - + const request = { restrict_new_user_invitations: true }; - + await expect(updateWorkspaceSettingsHandler(request, mockUser)).rejects.toThrow( - 'Only workspace admins can update settings' + 'Failed to update workspace settings' ); }); -}); \ No newline at end of file + + it('should update default datasets with specific IDs', async () => { + const request = { + default_datasets_ids: ['dataset-1', 'dataset-2'], + }; + + await updateWorkspaceSettingsHandler(request, mockUser); + + expect(securityUtils.updateDefaultDatasets).toHaveBeenCalledWith( + 'org-123', + ['dataset-1', 'dataset-2'], + mockUser.id + ); + expect(securityUtils.fetchDefaultDatasets).toHaveBeenCalledWith('org-123'); + }); + + it('should update default datasets with "all" keyword', async () => { + const request = { + default_datasets_ids: ['all'], + }; + + await updateWorkspaceSettingsHandler(request, mockUser); + + expect(securityUtils.updateDefaultDatasets).toHaveBeenCalledWith('org-123', 'all', mockUser.id); + }); + + it('should not update default datasets when not provided', async () => { + const request = { + restrict_new_user_invitations: true, + }; + + await updateWorkspaceSettingsHandler(request, mockUser); + + expect(securityUtils.updateDefaultDatasets).not.toHaveBeenCalled(); + // But should still fetch them for the response + expect(securityUtils.fetchDefaultDatasets).toHaveBeenCalledWith('org-123'); + }); + + it('should handle empty default datasets array', async () => { + const request = { + default_datasets_ids: [], + }; + + await updateWorkspaceSettingsHandler(request, mockUser); + + expect(securityUtils.updateDefaultDatasets).toHaveBeenCalledWith('org-123', [], mockUser.id); + }); +}); diff --git a/apps/server/src/api/v2/security/update-workspace-settings.ts b/apps/server/src/api/v2/security/update-workspace-settings.ts index 43fa6d77a..fc74c3b39 100644 --- a/apps/server/src/api/v2/security/update-workspace-settings.ts +++ b/apps/server/src/api/v2/security/update-workspace-settings.ts @@ -1,12 +1,15 @@ +import { type User, and, db, eq, isNull, organizations } from '@buster/database'; import type { UpdateWorkspaceSettingsRequest, UpdateWorkspaceSettingsResponse, } from '@buster/server-shared/security'; -import { type User, db, organizations, eq, and, isNull } from '@buster/database'; -import { - validateUserOrganization, - fetchOrganization, - checkWorkspaceAdminPermission +import { HTTPException } from 'hono/http-exception'; +import { + checkWorkspaceAdminPermission, + fetchDefaultDatasets, + fetchOrganization, + updateDefaultDatasets, + validateUserOrganization, } from './security-utils'; import { WorkspaceSettingsService } from './workspace-settings-service'; @@ -16,24 +19,72 @@ export async function updateWorkspaceSettingsHandler( request: UpdateWorkspaceSettingsRequest, user: User ): Promise { - // Validate user organization and permissions - const userOrg = await validateUserOrganization(user.id); - checkWorkspaceAdminPermission(userOrg.role); + try { + // Validate user organization and permissions + const userOrg = await validateUserOrganization(user.id); + checkWorkspaceAdminPermission(userOrg.role); - // Build update data - const updateData = settingsService.buildUpdateData(request); + // Build update data + const updateData = settingsService.buildUpdateData(request); - // Update organization settings - await updateOrganizationSettings(userOrg.organizationId, updateData); + // Update organization settings and default datasets concurrently + const updatePromises: Promise[] = [ + updateOrganizationSettings(userOrg.organizationId, updateData), + ]; - // Fetch updated organization - const updatedOrg = await fetchOrganization(userOrg.organizationId); + // Update default datasets if provided + if (request.default_datasets_ids !== undefined) { + updatePromises.push( + updateDefaultDatasets( + userOrg.organizationId, + request.default_datasets_ids.includes('all') + ? 'all' + : request.default_datasets_ids.filter((id): id is string => id !== 'all'), + user.id + ) + ); + } - // Return formatted response - return settingsService.formatWorkspaceSettingsResponse({ - restrictNewUserInvitations: updatedOrg.restrictNewUserInvitations, - defaultRole: updatedOrg.defaultRole, - }); + try { + await Promise.all(updatePromises); + } catch (error) { + console.error('Error during concurrent updates:', { + userId: user.id, + organizationId: userOrg.organizationId, + hasDatasetUpdate: request.default_datasets_ids !== undefined, + error: error instanceof Error ? error.message : error, + }); + throw error; // Re-throw to be caught by outer try-catch + } + + // Fetch updated organization and default datasets + const [updatedOrg, defaultDatasets] = await Promise.all([ + fetchOrganization(userOrg.organizationId), + fetchDefaultDatasets(userOrg.organizationId), + ]); + + // Return formatted response + return settingsService.formatWorkspaceSettingsResponse({ + restrictNewUserInvitations: updatedOrg.restrictNewUserInvitations, + defaultRole: updatedOrg.defaultRole, + defaultDatasets, + }); + } catch (error) { + console.error('Error in updateWorkspaceSettingsHandler:', { + userId: user.id, + request, + error: error instanceof Error ? error.message : error, + }); + + // Re-throw HTTPException as is, wrap other errors + if (error instanceof HTTPException) { + throw error; + } + + throw new HTTPException(500, { + message: 'Failed to update workspace settings', + }); + } } async function updateOrganizationSettings( @@ -44,13 +95,24 @@ async function updateOrganizationSettings( defaultRole?: string; } ): Promise { - await db - .update(organizations) - .set(updateData) - .where( - and( - eq(organizations.id, organizationId), - isNull(organizations.deletedAt) - ) - ); -} \ No newline at end of file + try { + await db + .update(organizations) + .set(updateData) + .where(and(eq(organizations.id, organizationId), isNull(organizations.deletedAt))); + + console.info('Updated organization settings:', { + organizationId, + fields: Object.keys(updateData).filter((k) => k !== 'updatedAt'), + }); + } catch (error) { + console.error('Error updating organization settings:', { + organizationId, + updateData, + error: error instanceof Error ? error.message : error, + }); + throw new HTTPException(500, { + message: 'Failed to update organization settings', + }); + } +} diff --git a/apps/server/src/api/v2/security/workspace-settings-service.test.ts b/apps/server/src/api/v2/security/workspace-settings-service.test.ts index 19430024f..aeded9976 100644 --- a/apps/server/src/api/v2/security/workspace-settings-service.test.ts +++ b/apps/server/src/api/v2/security/workspace-settings-service.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect } from 'vitest'; -import { WorkspaceSettingsService } from './workspace-settings-service'; import type { UpdateWorkspaceSettingsRequest } from '@buster/server-shared/security'; +import { describe, expect, it } from 'vitest'; +import { WorkspaceSettingsService } from './workspace-settings-service'; describe('WorkspaceSettingsService', () => { const service = new WorkspaceSettingsService(); @@ -37,15 +37,19 @@ describe('WorkspaceSettingsService', () => { restrictNewUserInvitations: false, defaultRole: 'member', }; - - expect(service.formatWorkspaceSettingsResponse(settingsTrue).restrict_new_user_invitations).toBe(true); - expect(service.formatWorkspaceSettingsResponse(settingsFalse).restrict_new_user_invitations).toBe(false); + + expect( + service.formatWorkspaceSettingsResponse(settingsTrue).restrict_new_user_invitations + ).toBe(true); + expect( + service.formatWorkspaceSettingsResponse(settingsFalse).restrict_new_user_invitations + ).toBe(false); }); it('should handle all string values correctly', () => { const roles = ['member', 'admin', 'workspace_admin', 'data_admin']; - - roles.forEach(role => { + + roles.forEach((role) => { const settings = { restrictNewUserInvitations: false, defaultRole: role, @@ -60,7 +64,7 @@ describe('WorkspaceSettingsService', () => { it('should include updatedAt timestamp', () => { const request: UpdateWorkspaceSettingsRequest = {}; const result = service.buildUpdateData(request); - + expect(result.updatedAt).toBeDefined(); expect(new Date(result.updatedAt).toISOString()).toBe(result.updatedAt); }); @@ -70,7 +74,7 @@ describe('WorkspaceSettingsService', () => { restrict_new_user_invitations: true, }; const result = service.buildUpdateData(request); - + expect(result).toHaveProperty('restrictNewUserInvitations', true); expect(result).not.toHaveProperty('defaultRole'); }); @@ -80,7 +84,7 @@ describe('WorkspaceSettingsService', () => { default_role: 'admin', }; const result = service.buildUpdateData(request); - + expect(result).toHaveProperty('defaultRole', 'admin'); expect(result).not.toHaveProperty('restrictNewUserInvitations'); }); @@ -91,7 +95,7 @@ describe('WorkspaceSettingsService', () => { default_role: 'member', }; const result = service.buildUpdateData(request); - + expect(result).toHaveProperty('restrictNewUserInvitations', false); expect(result).toHaveProperty('defaultRole', 'member'); expect(result).toHaveProperty('updatedAt'); @@ -103,7 +107,7 @@ describe('WorkspaceSettingsService', () => { default_role: undefined, }; const result = service.buildUpdateData(request); - + expect(result).not.toHaveProperty('restrictNewUserInvitations'); expect(result).not.toHaveProperty('defaultRole'); expect(result).toHaveProperty('updatedAt'); @@ -114,8 +118,8 @@ describe('WorkspaceSettingsService', () => { restrict_new_user_invitations: false, }; const result = service.buildUpdateData(request); - + expect(result.restrictNewUserInvitations).toBe(false); }); }); -}); \ No newline at end of file +}); diff --git a/apps/server/src/api/v2/security/workspace-settings-service.ts b/apps/server/src/api/v2/security/workspace-settings-service.ts index ec6a594d2..20511daca 100644 --- a/apps/server/src/api/v2/security/workspace-settings-service.ts +++ b/apps/server/src/api/v2/security/workspace-settings-service.ts @@ -1,17 +1,24 @@ -import type { +import type { GetWorkspaceSettingsResponse, - UpdateWorkspaceSettingsRequest + UpdateWorkspaceSettingsRequest, } from '@buster/server-shared/security'; export class WorkspaceSettingsService { formatWorkspaceSettingsResponse(settings: { restrictNewUserInvitations: boolean; - defaultRole: string; + defaultRole: + | 'workspace_admin' + | 'data_admin' + | 'querier' + | 'restricted_querier' + | 'viewer' + | 'none'; + defaultDatasets?: Array<{ id: string; name: string }>; }): GetWorkspaceSettingsResponse { return { restrict_new_user_invitations: settings.restrictNewUserInvitations, default_role: settings.defaultRole, - default_datasets: [], // TODO: Implement when datasets are available + default_datasets: settings.defaultDatasets || [], }; } @@ -38,4 +45,4 @@ export class WorkspaceSettingsService { return updateData; } -} \ No newline at end of file +} diff --git a/package.json b/package.json index ffa2d9f69..82307b8d5 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "ci:check": "pnpm run check && pnpm run typecheck", "db:check": "pnpm --filter @buster/database run db:check", "db:generate": "pnpm --filter @buster/database run db:generate", + "db:generate:custom": "pnpm --filter @buster/database run db:generate:custom", "db:init": "pnpm run --filter @buster/database db:init", "db:introspect": "pnpm --filter @buster/database run db:introspect", "db:migrate": "pnpm --filter @buster/database run db:migrate", diff --git a/packages/ai/tests/steps/unit/format-follow-up-message-step.test.ts b/packages/ai/tests/steps/unit/format-follow-up-message-step.test.ts index 6ab37a679..d50e36ea0 100644 --- a/packages/ai/tests/steps/unit/format-follow-up-message-step.test.ts +++ b/packages/ai/tests/steps/unit/format-follow-up-message-step.test.ts @@ -1,37 +1,39 @@ -import { describe, expect, test, vi } from 'vitest'; import type { CoreMessage } from 'ai'; +import { describe, expect, test, vi } from 'vitest'; import { formatFollowUpMessageStepExecution } from '../../../src/steps/post-processing/format-follow-up-message-step'; // Mock the agent and its dependencies vi.mock('@mastra/core', () => ({ Agent: vi.fn().mockImplementation(() => ({ generate: vi.fn().mockResolvedValue({ - toolCalls: [{ - toolName: 'generateUpdateMessage', - args: { - update_message: 'Test update message', - title: 'Update Title' - } - }] - }) + toolCalls: [ + { + toolName: 'generateUpdateMessage', + args: { + update_message: 'Test update message', + title: 'Update Title', + }, + }, + ], + }), })), - createStep: vi.fn((config) => config) + createStep: vi.fn((config) => config), })); vi.mock('braintrust', () => ({ - wrapTraced: vi.fn((fn) => fn) + wrapTraced: vi.fn((fn) => fn), })); vi.mock('../../../src/utils/models/anthropic-cached', () => ({ - anthropicCachedModel: vi.fn(() => 'mocked-model') + anthropicCachedModel: vi.fn(() => 'mocked-model'), })); vi.mock('../../../src/utils/standardizeMessages', () => ({ - standardizeMessages: vi.fn((msg) => [{ role: 'user', content: msg }]) + standardizeMessages: vi.fn((msg) => [{ role: 'user', content: msg }]), })); vi.mock('../../../src/tools/post-processing/generate-update-message', () => ({ - generateUpdateMessage: {} + generateUpdateMessage: {}, })); describe('Format Follow-up Message Step Unit Tests', () => { @@ -40,7 +42,7 @@ describe('Format Follow-up Message Step Unit Tests', () => { { role: 'user', content: 'Initial query about sales' }, { role: 'assistant', content: 'Sales data analysis complete' }, { role: 'user', content: 'Can you filter by last 6 months?' }, - { role: 'assistant', content: 'Filtered data shown' } + { role: 'assistant', content: 'Filtered data shown' }, ]; const inputData = { @@ -61,9 +63,9 @@ describe('Format Follow-up Message Step Unit Tests', () => { descriptiveTitle: 'Time Period Filter', classification: 'timePeriodInterpretation' as const, explanation: 'Assumed last 6 months means from today', - label: 'major' as const - } - ] + label: 'major' as const, + }, + ], }; const result = await formatFollowUpMessageStepExecution({ inputData }); @@ -93,9 +95,9 @@ describe('Format Follow-up Message Step Unit Tests', () => { descriptiveTitle: 'Data Aggregation', classification: 'aggregation' as const, explanation: 'Used SUM for totals', - label: 'major' as const - } - ] + label: 'major' as const, + }, + ], }; const result = await formatFollowUpMessageStepExecution({ inputData }); @@ -124,9 +126,9 @@ describe('Format Follow-up Message Step Unit Tests', () => { descriptiveTitle: 'Metric Definition', classification: 'metricDefinition' as const, explanation: 'Used standard revenue metric', - label: 'major' as const - } - ] + label: 'major' as const, + }, + ], }; const result = await formatFollowUpMessageStepExecution({ inputData }); @@ -141,7 +143,7 @@ describe('Format Follow-up Message Step Unit Tests', () => { const inputData = { conversationHistory: [ { role: 'user' as const, content: 'Follow-up question' }, - { role: 'assistant' as const, content: 'Follow-up answer' } + { role: 'assistant' as const, content: 'Follow-up answer' }, ], userName: 'Marcus Wright', messageId: 'msg-fu-999', @@ -157,15 +159,15 @@ describe('Format Follow-up Message Step Unit Tests', () => { descriptiveTitle: 'Formatting Choice', classification: 'dataFormat' as const, explanation: 'Used comma separation', - label: 'minor' as const + label: 'minor' as const, }, { descriptiveTitle: 'Time Zone', classification: 'timePeriodInterpretation' as const, explanation: 'Used UTC', - label: 'timeRelated' as const - } - ] + label: 'timeRelated' as const, + }, + ], }; const result = await formatFollowUpMessageStepExecution({ inputData }); @@ -180,7 +182,7 @@ describe('Format Follow-up Message Step Unit Tests', () => { const mockConversationHistory: CoreMessage[] = [ { role: 'user', content: 'Show me customer segments' }, { role: 'assistant', content: 'Here are the segments' }, - { role: 'user', content: 'Filter by enterprise only' } + { role: 'user', content: 'Filter by enterprise only' }, ]; const inputData = { @@ -201,15 +203,15 @@ describe('Format Follow-up Message Step Unit Tests', () => { descriptiveTitle: 'Enterprise Definition', classification: 'segmentDefinition' as const, explanation: 'Defined enterprise as >$1M revenue', - label: 'major' as const + label: 'major' as const, }, { descriptiveTitle: 'Customer Status', classification: 'dataQuality' as const, explanation: 'Included only active customers', - label: 'major' as const - } - ] + label: 'major' as const, + }, + ], }; const result = await formatFollowUpMessageStepExecution({ inputData }); @@ -219,4 +221,4 @@ describe('Format Follow-up Message Step Unit Tests', () => { expect(result.message).toBe('Test update message'); expect(result.conversationHistory).toEqual(mockConversationHistory); }); -}); \ No newline at end of file +}); diff --git a/packages/ai/tests/steps/unit/format-initial-message-step.test.ts b/packages/ai/tests/steps/unit/format-initial-message-step.test.ts index 6f6976466..dbbe7389c 100644 --- a/packages/ai/tests/steps/unit/format-initial-message-step.test.ts +++ b/packages/ai/tests/steps/unit/format-initial-message-step.test.ts @@ -1,44 +1,46 @@ -import { describe, expect, test, vi } from 'vitest'; import type { CoreMessage } from 'ai'; +import { describe, expect, test, vi } from 'vitest'; import { formatInitialMessageStepExecution } from '../../../src/steps/post-processing/format-initial-message-step'; // Mock the agent and its dependencies vi.mock('@mastra/core', () => ({ Agent: vi.fn().mockImplementation(() => ({ generate: vi.fn().mockResolvedValue({ - toolCalls: [{ - toolName: 'generateSummary', - args: { - summary_message: 'Test summary message', - title: 'Test Title' - } - }] - }) + toolCalls: [ + { + toolName: 'generateSummary', + args: { + summary_message: 'Test summary message', + title: 'Test Title', + }, + }, + ], + }), })), - createStep: vi.fn((config) => config) + createStep: vi.fn((config) => config), })); vi.mock('braintrust', () => ({ - wrapTraced: vi.fn((fn) => fn) + wrapTraced: vi.fn((fn) => fn), })); vi.mock('../../../src/utils/models/anthropic-cached', () => ({ - anthropicCachedModel: vi.fn(() => 'mocked-model') + anthropicCachedModel: vi.fn(() => 'mocked-model'), })); vi.mock('../../../src/utils/standardizeMessages', () => ({ - standardizeMessages: vi.fn((msg) => [{ role: 'user', content: msg }]) + standardizeMessages: vi.fn((msg) => [{ role: 'user', content: msg }]), })); vi.mock('../../../src/tools/post-processing/generate-summary', () => ({ - generateSummary: {} + generateSummary: {}, })); describe('Format Initial Message Step Unit Tests', () => { test('should include chat history in context message when present', async () => { const mockConversationHistory: CoreMessage[] = [ { role: 'user', content: 'What is the total revenue?' }, - { role: 'assistant', content: 'The total revenue is $1M' } + { role: 'assistant', content: 'The total revenue is $1M' }, ]; const inputData = { @@ -59,9 +61,9 @@ describe('Format Initial Message Step Unit Tests', () => { descriptiveTitle: 'Revenue Calculation', classification: 'metricDefinition' as const, explanation: 'Assumed revenue includes all sources', - label: 'major' as const - } - ] + label: 'major' as const, + }, + ], }; const result = await formatInitialMessageStepExecution({ inputData }); @@ -90,9 +92,9 @@ describe('Format Initial Message Step Unit Tests', () => { descriptiveTitle: 'Customer Count', classification: 'dataQuality' as const, explanation: 'Included all customer statuses', - label: 'major' as const - } - ] + label: 'major' as const, + }, + ], }; const result = await formatInitialMessageStepExecution({ inputData }); @@ -120,9 +122,9 @@ describe('Format Initial Message Step Unit Tests', () => { descriptiveTitle: 'Date Range', classification: 'timePeriodInterpretation' as const, explanation: 'Assumed current month', - label: 'major' as const - } - ] + label: 'major' as const, + }, + ], }; const result = await formatInitialMessageStepExecution({ inputData }); @@ -136,7 +138,7 @@ describe('Format Initial Message Step Unit Tests', () => { const inputData = { conversationHistory: [ { role: 'user' as const, content: 'Hello' }, - { role: 'assistant' as const, content: 'Hi there!' } + { role: 'assistant' as const, content: 'Hi there!' }, ], userName: 'Alice Cooper', messageId: 'msg-999', @@ -152,9 +154,9 @@ describe('Format Initial Message Step Unit Tests', () => { descriptiveTitle: 'Minor Detail', classification: 'dataFormat' as const, explanation: 'Used standard format', - label: 'minor' as const - } - ] + label: 'minor' as const, + }, + ], }; const result = await formatInitialMessageStepExecution({ inputData }); @@ -163,4 +165,4 @@ describe('Format Initial Message Step Unit Tests', () => { expect(result.summaryMessage).toBeUndefined(); expect(result.summaryTitle).toBeUndefined(); }); -}); \ No newline at end of file +}); diff --git a/packages/database/drizzle/0077_short_marvex.sql b/packages/database/drizzle/0077_short_marvex.sql new file mode 100644 index 000000000..1689d140e --- /dev/null +++ b/packages/database/drizzle/0077_short_marvex.sql @@ -0,0 +1,62 @@ +-- Custom SQL migration file, put your code below! -- +CREATE OR REPLACE FUNCTION public.auto_add_user_to_organizations() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +DECLARE + user_domain text; + org_record public.organizations%ROWTYPE; +BEGIN + IF EXISTS (SELECT 1 FROM public.users_to_organizations WHERE public.users_to_organizations.user_id = NEW.id) THEN + RETURN NEW; + END IF; + + user_domain := split_part(NEW.email, '@', 2); + + FOR org_record IN + SELECT * FROM public.organizations + WHERE user_domain = ANY(public.organizations.domains) + LOOP + INSERT INTO public.users_to_organizations ( + user_id, + organization_id, + role, + sharing_setting, + edit_sql, + upload_csv, + export_assets, + email_slack_enabled, + created_at, + updated_at, + deleted_at, + created_by, + updated_by, + deleted_by, + status + ) VALUES ( + NEW.id, + org_record.id, + org_record.default_role, + 'none'::public.sharing_setting_enum, + false, + false, + false, + false, + now(), + now(), + null, + NEW.id, + NEW.id, + null, + 'active'::public.user_organization_status_enum + ); + END LOOP; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER auto_add_to_orgs_trigger +AFTER INSERT ON public.users +FOR EACH ROW +EXECUTE FUNCTION public.auto_add_user_to_organizations(); \ No newline at end of file diff --git a/packages/database/drizzle/meta/0077_snapshot.json b/packages/database/drizzle/meta/0077_snapshot.json new file mode 100644 index 000000000..d776d4927 --- /dev/null +++ b/packages/database/drizzle/meta/0077_snapshot.json @@ -0,0 +1,6190 @@ +{ + "id": "69b60fbe-2657-452d-bd4b-b3a9925deb88", + "prevId": "3170cf76-f294-41e3-b4fe-e1c736607fce", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_organization_id_fkey": { + "name": "api_keys_organization_id_fkey", + "tableFrom": "api_keys", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "api_keys_owner_id_fkey": { + "name": "api_keys_owner_id_fkey", + "tableFrom": "api_keys", + "columnsFrom": [ + "owner_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_key": { + "name": "api_keys_key_key", + "columns": [ + "key" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset_permissions": { + "name": "asset_permissions", + "schema": "", + "columns": { + "identity_id": { + "name": "identity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "identity_type": { + "name": "identity_type", + "type": "identity_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_type": { + "name": "asset_type", + "type": "asset_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "asset_permission_role_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "asset_permissions_created_by_fkey": { + "name": "asset_permissions_created_by_fkey", + "tableFrom": "asset_permissions", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "asset_permissions_updated_by_fkey": { + "name": "asset_permissions_updated_by_fkey", + "tableFrom": "asset_permissions", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "asset_permissions_pkey": { + "name": "asset_permissions_pkey", + "columns": [ + "identity_id", + "identity_type", + "asset_id", + "asset_type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset_search": { + "name": "asset_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_type": { + "name": "asset_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "asset_search_asset_id_asset_type_idx": { + "name": "asset_search_asset_id_asset_type_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "asset_type", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pgroonga_content_index": { + "name": "pgroonga_content_index", + "columns": [ + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "pgroonga_text_full_text_search_ops_v2" + } + ], + "isUnique": false, + "with": {}, + "method": "pgroonga", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chats": { + "name": "chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "publicly_accessible": { + "name": "publicly_accessible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "publicly_enabled_by": { + "name": "publicly_enabled_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "public_expiry_date": { + "name": "public_expiry_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "most_recent_file_id": { + "name": "most_recent_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "most_recent_file_type": { + "name": "most_recent_file_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "most_recent_version_number": { + "name": "most_recent_version_number", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chats_created_at_idx": { + "name": "chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "chats_created_by_idx": { + "name": "chats_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "chats_organization_id_idx": { + "name": "chats_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_chats_most_recent_file_id": { + "name": "idx_chats_most_recent_file_id", + "columns": [ + { + "expression": "most_recent_file_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_chats_most_recent_file_type": { + "name": "idx_chats_most_recent_file_type", + "columns": [ + { + "expression": "most_recent_file_type", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "chats_organization_id_fkey": { + "name": "chats_organization_id_fkey", + "tableFrom": "chats", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "chats_created_by_fkey": { + "name": "chats_created_by_fkey", + "tableFrom": "chats", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "chats_updated_by_fkey": { + "name": "chats_updated_by_fkey", + "tableFrom": "chats", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "chats_publicly_enabled_by_fkey": { + "name": "chats_publicly_enabled_by_fkey", + "tableFrom": "chats", + "columnsFrom": [ + "publicly_enabled_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collections": { + "name": "collections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "collections_organization_id_fkey": { + "name": "collections_organization_id_fkey", + "tableFrom": "collections", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "collections_created_by_fkey": { + "name": "collections_created_by_fkey", + "tableFrom": "collections", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "collections_updated_by_fkey": { + "name": "collections_updated_by_fkey", + "tableFrom": "collections", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collections_to_assets": { + "name": "collections_to_assets", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_type": { + "name": "asset_type", + "type": "asset_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "collections_to_assets_created_by_fkey": { + "name": "collections_to_assets_created_by_fkey", + "tableFrom": "collections_to_assets", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "collections_to_assets_updated_by_fkey": { + "name": "collections_to_assets_updated_by_fkey", + "tableFrom": "collections_to_assets", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "collections_to_assets_pkey": { + "name": "collections_to_assets_pkey", + "columns": [ + "collection_id", + "asset_id", + "asset_type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_files": { + "name": "dashboard_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "filter": { + "name": "filter", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "publicly_accessible": { + "name": "publicly_accessible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "publicly_enabled_by": { + "name": "publicly_enabled_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "public_expiry_date": { + "name": "public_expiry_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "version_history": { + "name": "version_history", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "public_password": { + "name": "public_password", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "dashboard_files_created_by_idx": { + "name": "dashboard_files_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "dashboard_files_deleted_at_idx": { + "name": "dashboard_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "dashboard_files_organization_id_idx": { + "name": "dashboard_files_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "dashboard_files_created_by_fkey": { + "name": "dashboard_files_created_by_fkey", + "tableFrom": "dashboard_files", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "dashboard_files_publicly_enabled_by_fkey": { + "name": "dashboard_files_publicly_enabled_by_fkey", + "tableFrom": "dashboard_files", + "columnsFrom": [ + "publicly_enabled_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_versions": { + "name": "dashboard_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_versions_dashboard_id_fkey": { + "name": "dashboard_versions_dashboard_id_fkey", + "tableFrom": "dashboard_versions", + "columnsFrom": [ + "dashboard_id" + ], + "tableTo": "dashboards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboards": { + "name": "dashboards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "publicly_accessible": { + "name": "publicly_accessible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "publicly_enabled_by": { + "name": "publicly_enabled_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "public_expiry_date": { + "name": "public_expiry_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "password_secret_id": { + "name": "password_secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "dashboards_publicly_enabled_by_fkey": { + "name": "dashboards_publicly_enabled_by_fkey", + "tableFrom": "dashboards", + "columnsFrom": [ + "publicly_enabled_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "dashboards_organization_id_fkey": { + "name": "dashboards_organization_id_fkey", + "tableFrom": "dashboards", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "dashboards_created_by_fkey": { + "name": "dashboards_created_by_fkey", + "tableFrom": "dashboards", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "dashboards_updated_by_fkey": { + "name": "dashboards_updated_by_fkey", + "tableFrom": "dashboards", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_sources": { + "name": "data_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "onboarding_status": { + "name": "onboarding_status", + "type": "data_source_onboarding_status_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'notStarted'" + }, + "onboarding_error": { + "name": "onboarding_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'dev'" + } + }, + "indexes": {}, + "foreignKeys": { + "data_sources_organization_id_fkey": { + "name": "data_sources_organization_id_fkey", + "tableFrom": "data_sources", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "data_sources_created_by_fkey": { + "name": "data_sources_created_by_fkey", + "tableFrom": "data_sources", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "data_sources_updated_by_fkey": { + "name": "data_sources_updated_by_fkey", + "tableFrom": "data_sources", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "data_sources_name_organization_id_env_key": { + "name": "data_sources_name_organization_id_env_key", + "columns": [ + "name", + "organization_id", + "env" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.database_metadata": { + "name": "database_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "data_source_id": { + "name": "data_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "database_metadata_data_source_id_idx": { + "name": "database_metadata_data_source_id_idx", + "columns": [ + { + "expression": "data_source_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "database_metadata_data_source_id_fkey": { + "name": "database_metadata_data_source_id_fkey", + "tableFrom": "database_metadata", + "columnsFrom": [ + "data_source_id" + ], + "tableTo": "data_sources", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "database_metadata_data_source_id_name_key": { + "name": "database_metadata_data_source_id_name_key", + "columns": [ + "data_source_id", + "name" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dataset_columns": { + "name": "dataset_columns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "dataset_id": { + "name": "dataset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nullable": { + "name": "nullable", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stored_values": { + "name": "stored_values", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "stored_values_status": { + "name": "stored_values_status", + "type": "stored_values_status_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "stored_values_error": { + "name": "stored_values_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stored_values_count": { + "name": "stored_values_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "stored_values_last_synced": { + "name": "stored_values_last_synced", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "semantic_type": { + "name": "semantic_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dim_type": { + "name": "dim_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expr": { + "name": "expr", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_dataset_column_name": { + "name": "unique_dataset_column_name", + "columns": [ + "dataset_id", + "name" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dataset_groups": { + "name": "dataset_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "dataset_groups_deleted_at_idx": { + "name": "dataset_groups_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "dataset_groups_organization_id_idx": { + "name": "dataset_groups_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "dataset_groups_organization_id_fkey": { + "name": "dataset_groups_organization_id_fkey", + "tableFrom": "dataset_groups", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "dataset_groups_policy": { + "name": "dataset_groups_policy", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "authenticated" + ], + "using": "true" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dataset_groups_permissions": { + "name": "dataset_groups_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dataset_group_id": { + "name": "dataset_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "dataset_groups_permissions_dataset_group_id_idx": { + "name": "dataset_groups_permissions_dataset_group_id_idx", + "columns": [ + { + "expression": "dataset_group_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "dataset_groups_permissions_organization_id_idx": { + "name": "dataset_groups_permissions_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "dataset_groups_permissions_permission_id_idx": { + "name": "dataset_groups_permissions_permission_id_idx", + "columns": [ + { + "expression": "permission_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "dataset_groups_permissions_dataset_group_id_fkey": { + "name": "dataset_groups_permissions_dataset_group_id_fkey", + "tableFrom": "dataset_groups_permissions", + "columnsFrom": [ + "dataset_group_id" + ], + "tableTo": "dataset_groups", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "dataset_groups_permissions_organization_id_fkey": { + "name": "dataset_groups_permissions_organization_id_fkey", + "tableFrom": "dataset_groups_permissions", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_dataset_group_permission": { + "name": "unique_dataset_group_permission", + "columns": [ + "dataset_group_id", + "permission_id", + "permission_type" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dataset_permissions": { + "name": "dataset_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dataset_id": { + "name": "dataset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "dataset_permissions_dataset_id_idx": { + "name": "dataset_permissions_dataset_id_idx", + "columns": [ + { + "expression": "dataset_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "dataset_permissions_deleted_at_idx": { + "name": "dataset_permissions_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "dataset_permissions_organization_id_idx": { + "name": "dataset_permissions_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "dataset_permissions_permission_lookup_idx": { + "name": "dataset_permissions_permission_lookup_idx", + "columns": [ + { + "expression": "permission_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "dataset_permissions_organization_id_fkey": { + "name": "dataset_permissions_organization_id_fkey", + "tableFrom": "dataset_permissions", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "dataset_permissions_dataset_id_fkey": { + "name": "dataset_permissions_dataset_id_fkey", + "tableFrom": "dataset_permissions", + "columnsFrom": [ + "dataset_id" + ], + "tableTo": "datasets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dataset_permissions_dataset_id_permission_id_permission_typ_key": { + "name": "dataset_permissions_dataset_id_permission_id_permission_typ_key", + "columns": [ + "dataset_id", + "permission_id", + "permission_type" + ], + "nullsNotDistinct": false + } + }, + "policies": { + "dataset_permissions_policy": { + "name": "dataset_permissions_policy", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "authenticated" + ], + "using": "true" + } + }, + "checkConstraints": { + "dataset_permissions_permission_type_check": { + "name": "dataset_permissions_permission_type_check", + "value": "(permission_type)::text = ANY ((ARRAY['user'::character varying, 'dataset_group'::character varying, 'permission_group'::character varying])::text[])" + } + }, + "isRLSEnabled": false + }, + "public.datasets": { + "name": "datasets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "when_to_use": { + "name": "when_to_use", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "when_not_to_use": { + "name": "when_not_to_use", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "dataset_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "imported": { + "name": "imported", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data_source_id": { + "name": "data_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "yml_file": { + "name": "yml_file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_identifier": { + "name": "database_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "datasets_data_source_id_fkey": { + "name": "datasets_data_source_id_fkey", + "tableFrom": "datasets", + "columnsFrom": [ + "data_source_id" + ], + "tableTo": "data_sources", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "datasets_organization_id_fkey": { + "name": "datasets_organization_id_fkey", + "tableFrom": "datasets", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "datasets_created_by_fkey": { + "name": "datasets_created_by_fkey", + "tableFrom": "datasets", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "datasets_updated_by_fkey": { + "name": "datasets_updated_by_fkey", + "tableFrom": "datasets", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "datasets_database_name_data_source_id_key": { + "name": "datasets_database_name_data_source_id_key", + "columns": [ + "database_name", + "data_source_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.datasets_to_dataset_groups": { + "name": "datasets_to_dataset_groups", + "schema": "", + "columns": { + "dataset_id": { + "name": "dataset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dataset_group_id": { + "name": "dataset_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "datasets_to_dataset_groups_dataset_group_id_idx": { + "name": "datasets_to_dataset_groups_dataset_group_id_idx", + "columns": [ + { + "expression": "dataset_group_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "datasets_to_dataset_groups_dataset_id_fkey": { + "name": "datasets_to_dataset_groups_dataset_id_fkey", + "tableFrom": "datasets_to_dataset_groups", + "columnsFrom": [ + "dataset_id" + ], + "tableTo": "datasets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "datasets_to_dataset_groups_dataset_group_id_fkey": { + "name": "datasets_to_dataset_groups_dataset_group_id_fkey", + "tableFrom": "datasets_to_dataset_groups", + "columnsFrom": [ + "dataset_group_id" + ], + "tableTo": "dataset_groups", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "datasets_to_dataset_groups_pkey": { + "name": "datasets_to_dataset_groups_pkey", + "columns": [ + "dataset_id", + "dataset_group_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": { + "datasets_to_dataset_groups_policy": { + "name": "datasets_to_dataset_groups_policy", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "authenticated" + ], + "using": "true" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.datasets_to_permission_groups": { + "name": "datasets_to_permission_groups", + "schema": "", + "columns": { + "dataset_id": { + "name": "dataset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "datasets_to_permission_groups_dataset_id_fkey": { + "name": "datasets_to_permission_groups_dataset_id_fkey", + "tableFrom": "datasets_to_permission_groups", + "columnsFrom": [ + "dataset_id" + ], + "tableTo": "datasets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "datasets_to_permission_groups_permission_group_id_fkey": { + "name": "datasets_to_permission_groups_permission_group_id_fkey", + "tableFrom": "datasets_to_permission_groups", + "columnsFrom": [ + "permission_group_id" + ], + "tableTo": "permission_groups", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "datasets_to_permission_groups_pkey": { + "name": "datasets_to_permission_groups_pkey", + "columns": [ + "dataset_id", + "permission_group_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": { + "datasets_to_permission_groups_policy": { + "name": "datasets_to_permission_groups_policy", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "authenticated" + ], + "using": "true" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.__diesel_schema_migrations": { + "name": "__diesel_schema_migrations", + "schema": "", + "columns": { + "version": { + "name": "version", + "type": "varchar(50)", + "primaryKey": true, + "notNull": true + }, + "run_on": { + "name": "run_on", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "diesel_schema_migrations_policy": { + "name": "diesel_schema_migrations_policy", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "authenticated" + ], + "using": "true" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_relationship": { + "name": "entity_relationship", + "schema": "", + "columns": { + "primary_dataset_id": { + "name": "primary_dataset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "foreign_dataset_id": { + "name": "foreign_dataset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "relationship_type": { + "name": "relationship_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "entity_relationship_pkey": { + "name": "entity_relationship_pkey", + "columns": [ + "primary_dataset_id", + "foreign_dataset_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "request_message": { + "name": "request_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_messages": { + "name": "response_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "raw_llm_messages": { + "name": "raw_llm_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "final_reasoning_message": { + "name": "final_reasoning_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_completed": { + "name": "is_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "post_processing_message": { + "name": "post_processing_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_chat_id_idx": { + "name": "messages_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "messages_created_at_idx": { + "name": "messages_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "messages_created_by_idx": { + "name": "messages_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "messages_chat_id_fkey": { + "name": "messages_chat_id_fkey", + "tableFrom": "messages", + "columnsFrom": [ + "chat_id" + ], + "tableTo": "chats", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "messages_created_by_fkey": { + "name": "messages_created_by_fkey", + "tableFrom": "messages", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages_deprecated": { + "name": "messages_deprecated", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sent_by": { + "name": "sent_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "responses": { + "name": "responses", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback": { + "name": "feedback", + "type": "message_feedback_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "verification": { + "name": "verification", + "type": "verification_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'notRequested'" + }, + "dataset_id": { + "name": "dataset_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chart_config": { + "name": "chart_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "chart_recommendations": { + "name": "chart_recommendations", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "time_frame": { + "name": "time_frame", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data_metadata": { + "name": "data_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_state": { + "name": "draft_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "summary_question": { + "name": "summary_question", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sql_evaluation_id": { + "name": "sql_evaluation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "messages_sent_by_fkey": { + "name": "messages_sent_by_fkey", + "tableFrom": "messages_deprecated", + "columnsFrom": [ + "sent_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "messages_dataset_id_fkey": { + "name": "messages_dataset_id_fkey", + "tableFrom": "messages_deprecated", + "columnsFrom": [ + "dataset_id" + ], + "tableTo": "datasets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_deprecated_sent_by_fkey": { + "name": "messages_deprecated_sent_by_fkey", + "tableFrom": "messages_deprecated", + "columnsFrom": [ + "sent_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages_to_files": { + "name": "messages_to_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "is_duplicate": { + "name": "is_duplicate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "messages_files_file_id_idx": { + "name": "messages_files_file_id_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "messages_files_message_id_idx": { + "name": "messages_files_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "messages_to_files_message_id_fkey": { + "name": "messages_to_files_message_id_fkey", + "tableFrom": "messages_to_files", + "columnsFrom": [ + "message_id" + ], + "tableTo": "messages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "messages_to_files_message_id_file_id_key": { + "name": "messages_to_files_message_id_file_id_key", + "columns": [ + "message_id", + "file_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages_to_slack_messages": { + "name": "messages_to_slack_messages", + "schema": "", + "columns": { + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slack_message_id": { + "name": "slack_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_to_slack_messages_message_id_idx": { + "name": "messages_to_slack_messages_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "messages_to_slack_messages_slack_message_id_idx": { + "name": "messages_to_slack_messages_slack_message_id_idx", + "columns": [ + { + "expression": "slack_message_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "messages_to_slack_messages_message_id_fkey": { + "name": "messages_to_slack_messages_message_id_fkey", + "tableFrom": "messages_to_slack_messages", + "columnsFrom": [ + "message_id" + ], + "tableTo": "messages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "messages_to_slack_messages_slack_message_id_fkey": { + "name": "messages_to_slack_messages_slack_message_id_fkey", + "tableFrom": "messages_to_slack_messages", + "columnsFrom": [ + "slack_message_id" + ], + "tableTo": "slack_message_tracking", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "messages_to_slack_messages_pkey": { + "name": "messages_to_slack_messages_pkey", + "columns": [ + "message_id", + "slack_message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metric_files": { + "name": "metric_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "verification_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'notRequested'" + }, + "evaluation_obj": { + "name": "evaluation_obj", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "evaluation_summary": { + "name": "evaluation_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evaluation_score": { + "name": "evaluation_score", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "publicly_accessible": { + "name": "publicly_accessible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "publicly_enabled_by": { + "name": "publicly_enabled_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "public_expiry_date": { + "name": "public_expiry_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "version_history": { + "name": "version_history", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "data_metadata": { + "name": "data_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "public_password": { + "name": "public_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data_source_id": { + "name": "data_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "metric_files_created_by_idx": { + "name": "metric_files_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "metric_files_data_metadata_idx": { + "name": "metric_files_data_metadata_idx", + "columns": [ + { + "expression": "data_metadata", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "jsonb_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + }, + "metric_files_deleted_at_idx": { + "name": "metric_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "metric_files_organization_id_idx": { + "name": "metric_files_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "metric_files_created_by_fkey": { + "name": "metric_files_created_by_fkey", + "tableFrom": "metric_files", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "metric_files_publicly_enabled_by_fkey": { + "name": "metric_files_publicly_enabled_by_fkey", + "tableFrom": "metric_files", + "columnsFrom": [ + "publicly_enabled_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "fk_data_source": { + "name": "fk_data_source", + "tableFrom": "metric_files", + "columnsFrom": [ + "data_source_id" + ], + "tableTo": "data_sources", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metric_files_to_dashboard_files": { + "name": "metric_files_to_dashboard_files", + "schema": "", + "columns": { + "metric_file_id": { + "name": "metric_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dashboard_file_id": { + "name": "dashboard_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "metric_files_to_dashboard_files_dashboard_id_idx": { + "name": "metric_files_to_dashboard_files_dashboard_id_idx", + "columns": [ + { + "expression": "dashboard_file_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "metric_files_to_dashboard_files_deleted_at_idx": { + "name": "metric_files_to_dashboard_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "metric_files_to_dashboard_files_metric_id_idx": { + "name": "metric_files_to_dashboard_files_metric_id_idx", + "columns": [ + { + "expression": "metric_file_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "metric_files_to_dashboard_files_metric_file_id_fkey": { + "name": "metric_files_to_dashboard_files_metric_file_id_fkey", + "tableFrom": "metric_files_to_dashboard_files", + "columnsFrom": [ + "metric_file_id" + ], + "tableTo": "metric_files", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "metric_files_to_dashboard_files_dashboard_file_id_fkey": { + "name": "metric_files_to_dashboard_files_dashboard_file_id_fkey", + "tableFrom": "metric_files_to_dashboard_files", + "columnsFrom": [ + "dashboard_file_id" + ], + "tableTo": "dashboard_files", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "metric_files_to_dashboard_files_created_by_fkey": { + "name": "metric_files_to_dashboard_files_created_by_fkey", + "tableFrom": "metric_files_to_dashboard_files", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "metric_files_to_dashboard_files_pkey": { + "name": "metric_files_to_dashboard_files_pkey", + "columns": [ + "metric_file_id", + "dashboard_file_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metric_files_to_datasets": { + "name": "metric_files_to_datasets", + "schema": "", + "columns": { + "metric_file_id": { + "name": "metric_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dataset_id": { + "name": "dataset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric_version_number": { + "name": "metric_version_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fk_metric_file": { + "name": "fk_metric_file", + "tableFrom": "metric_files_to_datasets", + "columnsFrom": [ + "metric_file_id" + ], + "tableTo": "metric_files", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "fk_dataset": { + "name": "fk_dataset", + "tableFrom": "metric_files_to_datasets", + "columnsFrom": [ + "dataset_id" + ], + "tableTo": "datasets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "metric_files_to_datasets_pkey": { + "name": "metric_files_to_datasets_pkey", + "columns": [ + "metric_file_id", + "dataset_id", + "metric_version_number" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_required": { + "name": "payment_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "domains": { + "name": "domains", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "restrict_new_user_invitations": { + "name": "restrict_new_user_invitations", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "defaultRole": { + "name": "defaultRole", + "type": "user_organization_role_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'restricted_querier'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_name_key": { + "name": "organizations_name_key", + "columns": [ + "name" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_groups": { + "name": "permission_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "permission_groups_organization_id_fkey": { + "name": "permission_groups_organization_id_fkey", + "tableFrom": "permission_groups", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "permission_groups_created_by_fkey": { + "name": "permission_groups_created_by_fkey", + "tableFrom": "permission_groups", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "permission_groups_updated_by_fkey": { + "name": "permission_groups_updated_by_fkey", + "tableFrom": "permission_groups", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_groups_to_identities": { + "name": "permission_groups_to_identities", + "schema": "", + "columns": { + "permission_group_id": { + "name": "permission_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "identity_type": { + "name": "identity_type", + "type": "identity_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "permission_groups_to_identities_created_by_fkey": { + "name": "permission_groups_to_identities_created_by_fkey", + "tableFrom": "permission_groups_to_identities", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "permission_groups_to_identities_updated_by_fkey": { + "name": "permission_groups_to_identities_updated_by_fkey", + "tableFrom": "permission_groups_to_identities", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "permission_groups_to_identities_pkey": { + "name": "permission_groups_to_identities_pkey", + "columns": [ + "permission_group_id", + "identity_id", + "identity_type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_groups_to_users": { + "name": "permission_groups_to_users", + "schema": "", + "columns": { + "permission_group_id": { + "name": "permission_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_groups_to_users_user_id_idx": { + "name": "permission_groups_to_users_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "permission_groups_to_users_permission_group_id_fkey": { + "name": "permission_groups_to_users_permission_group_id_fkey", + "tableFrom": "permission_groups_to_users", + "columnsFrom": [ + "permission_group_id" + ], + "tableTo": "permission_groups", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "permission_groups_to_users_user_id_fkey": { + "name": "permission_groups_to_users_user_id_fkey", + "tableFrom": "permission_groups_to_users", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "permission_groups_to_users_pkey": { + "name": "permission_groups_to_users_pkey", + "columns": [ + "permission_group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": { + "permission_groups_to_users_policy": { + "name": "permission_groups_to_users_policy", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "authenticated" + ], + "using": "true" + } + }, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schema_metadata": { + "name": "schema_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "data_source_id": { + "name": "data_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "schema_metadata_data_source_id_idx": { + "name": "schema_metadata_data_source_id_idx", + "columns": [ + { + "expression": "data_source_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "schema_metadata_database_id_idx": { + "name": "schema_metadata_database_id_idx", + "columns": [ + { + "expression": "database_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "schema_metadata_data_source_id_fkey": { + "name": "schema_metadata_data_source_id_fkey", + "tableFrom": "schema_metadata", + "columnsFrom": [ + "data_source_id" + ], + "tableTo": "data_sources", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "schema_metadata_database_id_fkey": { + "name": "schema_metadata_database_id_fkey", + "tableFrom": "schema_metadata", + "columnsFrom": [ + "database_id" + ], + "tableTo": "database_metadata", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schema_metadata_data_source_id_database_id_name_key": { + "name": "schema_metadata_data_source_id_database_id_name_key", + "columns": [ + "data_source_id", + "database_id", + "name" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_integrations": { + "name": "slack_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_state": { + "name": "oauth_state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "oauth_expires_at": { + "name": "oauth_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "oauth_metadata": { + "name": "oauth_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "team_id": { + "name": "team_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "team_domain": { + "name": "team_domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_vault_key": { + "name": "token_vault_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "installed_by_slack_user_id": { + "name": "installed_by_slack_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "slack_integration_status_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "default_channel": { + "name": "default_channel", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_slack_integrations_org_id": { + "name": "idx_slack_integrations_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_slack_integrations_team_id": { + "name": "idx_slack_integrations_team_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_slack_integrations_oauth_state": { + "name": "idx_slack_integrations_oauth_state", + "columns": [ + { + "expression": "oauth_state", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_slack_integrations_oauth_expires": { + "name": "idx_slack_integrations_oauth_expires", + "columns": [ + { + "expression": "oauth_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "slack_integrations_organization_id_fkey": { + "name": "slack_integrations_organization_id_fkey", + "tableFrom": "slack_integrations", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "slack_integrations_user_id_fkey": { + "name": "slack_integrations_user_id_fkey", + "tableFrom": "slack_integrations", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "slack_integrations_oauth_state_unique": { + "name": "slack_integrations_oauth_state_unique", + "columns": [ + "oauth_state" + ], + "nullsNotDistinct": false + }, + "slack_integrations_token_vault_key_unique": { + "name": "slack_integrations_token_vault_key_unique", + "columns": [ + "token_vault_key" + ], + "nullsNotDistinct": false + }, + "slack_integrations_org_team_key": { + "name": "slack_integrations_org_team_key", + "columns": [ + "organization_id", + "team_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "slack_integrations_status_check": { + "name": "slack_integrations_status_check", + "value": "(status = 'pending' AND oauth_state IS NOT NULL) OR (status != 'pending' AND team_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.slack_message_tracking": { + "name": "slack_message_tracking", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "internal_message_id": { + "name": "internal_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slack_message_ts": { + "name": "slack_message_ts", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slack_thread_ts": { + "name": "slack_thread_ts", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "message_type": { + "name": "message_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_info": { + "name": "sender_info", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_message_tracking_integration": { + "name": "idx_message_tracking_integration", + "columns": [ + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_message_tracking_channel": { + "name": "idx_message_tracking_channel", + "columns": [ + { + "expression": "slack_channel_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_message_tracking_thread": { + "name": "idx_message_tracking_thread", + "columns": [ + { + "expression": "slack_thread_ts", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "slack_message_tracking_integration_id_fkey": { + "name": "slack_message_tracking_integration_id_fkey", + "tableFrom": "slack_message_tracking", + "columnsFrom": [ + "integration_id" + ], + "tableTo": "slack_integrations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "slack_message_tracking_internal_message_id_unique": { + "name": "slack_message_tracking_internal_message_id_unique", + "columns": [ + "internal_message_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sql_evaluations": { + "name": "sql_evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v4()" + }, + "evaluation_obj": { + "name": "evaluation_obj", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "evaluation_summary": { + "name": "evaluation_summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stored_values_sync_jobs": { + "name": "stored_values_sync_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "data_source_id": { + "name": "data_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_name": { + "name": "schema_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "column_name": { + "name": "column_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_stored_values_sync_jobs_data_source_id": { + "name": "idx_stored_values_sync_jobs_data_source_id", + "columns": [ + { + "expression": "data_source_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_stored_values_sync_jobs_db_schema_table_column": { + "name": "idx_stored_values_sync_jobs_db_schema_table_column", + "columns": [ + { + "expression": "database_name", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "schema_name", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "table_name", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "column_name", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_stored_values_sync_jobs_status": { + "name": "idx_stored_values_sync_jobs_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "stored_values_sync_jobs_data_source_id_fkey": { + "name": "stored_values_sync_jobs_data_source_id_fkey", + "tableFrom": "stored_values_sync_jobs", + "columnsFrom": [ + "data_source_id" + ], + "tableTo": "data_sources", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_metadata": { + "name": "table_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "data_source_id": { + "name": "data_source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "schema_id": { + "name": "schema_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_name": { + "name": "schema_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "database_name": { + "name": "database_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "table_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "row_count": { + "name": "row_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clustering_keys": { + "name": "clustering_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "columns": { + "name": "columns", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_metadata_data_source_id_idx": { + "name": "table_metadata_data_source_id_idx", + "columns": [ + { + "expression": "data_source_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "table_metadata_database_id_idx": { + "name": "table_metadata_database_id_idx", + "columns": [ + { + "expression": "database_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "table_metadata_schema_id_idx": { + "name": "table_metadata_schema_id_idx", + "columns": [ + { + "expression": "schema_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "uuid_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "table_metadata_data_source_id_fkey": { + "name": "table_metadata_data_source_id_fkey", + "tableFrom": "table_metadata", + "columnsFrom": [ + "data_source_id" + ], + "tableTo": "data_sources", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "table_metadata_database_id_fkey": { + "name": "table_metadata_database_id_fkey", + "tableFrom": "table_metadata", + "columnsFrom": [ + "database_id" + ], + "tableTo": "database_metadata", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "table_metadata_schema_id_fkey": { + "name": "table_metadata_schema_id_fkey", + "tableFrom": "table_metadata", + "columnsFrom": [ + "schema_id" + ], + "tableTo": "schema_metadata", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "table_metadata_data_source_id_schema_id_name_key": { + "name": "table_metadata_data_source_id_schema_id_name_key", + "columns": [ + "data_source_id", + "schema_id", + "name" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sharing_setting": { + "name": "sharing_setting", + "type": "sharing_setting_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "edit_sql": { + "name": "edit_sql", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "upload_csv": { + "name": "upload_csv", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "export_assets": { + "name": "export_assets", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_slack_enabled": { + "name": "email_slack_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "teams_organization_id_fkey": { + "name": "teams_organization_id_fkey", + "tableFrom": "teams", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "teams_created_by_fkey": { + "name": "teams_created_by_fkey", + "tableFrom": "teams", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_name_key": { + "name": "teams_name_key", + "columns": [ + "name" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams_to_users": { + "name": "teams_to_users", + "schema": "", + "columns": { + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "team_role_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "teams_to_users_team_id_fkey": { + "name": "teams_to_users_team_id_fkey", + "tableFrom": "teams_to_users", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "teams_to_users_user_id_fkey": { + "name": "teams_to_users_user_id_fkey", + "tableFrom": "teams_to_users", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "teams_to_users_pkey": { + "name": "teams_to_users_pkey", + "columns": [ + "team_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.terms": { + "name": "terms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sql_snippet": { + "name": "sql_snippet", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "terms_organization_id_fkey": { + "name": "terms_organization_id_fkey", + "tableFrom": "terms", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "terms_created_by_fkey": { + "name": "terms_created_by_fkey", + "tableFrom": "terms", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "terms_updated_by_fkey": { + "name": "terms_updated_by_fkey", + "tableFrom": "terms", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.terms_to_datasets": { + "name": "terms_to_datasets", + "schema": "", + "columns": { + "term_id": { + "name": "term_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dataset_id": { + "name": "dataset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "terms_to_datasets_term_id_fkey": { + "name": "terms_to_datasets_term_id_fkey", + "tableFrom": "terms_to_datasets", + "columnsFrom": [ + "term_id" + ], + "tableTo": "terms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "terms_to_datasets_dataset_id_fkey": { + "name": "terms_to_datasets_dataset_id_fkey", + "tableFrom": "terms_to_datasets", + "columnsFrom": [ + "dataset_id" + ], + "tableTo": "datasets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "terms_to_datasets_pkey": { + "name": "terms_to_datasets_pkey", + "columns": [ + "term_id", + "dataset_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.threads_deprecated": { + "name": "threads_deprecated", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "publicly_accessible": { + "name": "publicly_accessible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "publicly_enabled_by": { + "name": "publicly_enabled_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "public_expiry_date": { + "name": "public_expiry_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "password_secret_id": { + "name": "password_secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "state_message_id": { + "name": "state_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_thread_id": { + "name": "parent_thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "threads_created_by_fkey": { + "name": "threads_created_by_fkey", + "tableFrom": "threads_deprecated", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "threads_updated_by_fkey": { + "name": "threads_updated_by_fkey", + "tableFrom": "threads_deprecated", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "threads_publicly_enabled_by_fkey": { + "name": "threads_publicly_enabled_by_fkey", + "tableFrom": "threads_deprecated", + "columnsFrom": [ + "publicly_enabled_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "threads_parent_thread_id_fkey": { + "name": "threads_parent_thread_id_fkey", + "tableFrom": "threads_deprecated", + "columnsFrom": [ + "parent_thread_id" + ], + "tableTo": "threads_deprecated", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "threads_organization_id_fkey": { + "name": "threads_organization_id_fkey", + "tableFrom": "threads_deprecated", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "threads_deprecated_created_by_fkey": { + "name": "threads_deprecated_created_by_fkey", + "tableFrom": "threads_deprecated", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "threads_deprecated_updated_by_fkey": { + "name": "threads_deprecated_updated_by_fkey", + "tableFrom": "threads_deprecated", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "threads_deprecated_publicly_enabled_by_fkey": { + "name": "threads_deprecated_publicly_enabled_by_fkey", + "tableFrom": "threads_deprecated", + "columnsFrom": [ + "publicly_enabled_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.threads_to_dashboards": { + "name": "threads_to_dashboards", + "schema": "", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "added_by": { + "name": "added_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "threads_to_dashboards_thread_id_fkey": { + "name": "threads_to_dashboards_thread_id_fkey", + "tableFrom": "threads_to_dashboards", + "columnsFrom": [ + "thread_id" + ], + "tableTo": "threads_deprecated", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "threads_to_dashboards_dashboard_id_fkey": { + "name": "threads_to_dashboards_dashboard_id_fkey", + "tableFrom": "threads_to_dashboards", + "columnsFrom": [ + "dashboard_id" + ], + "tableTo": "dashboards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "threads_to_dashboards_added_by_fkey": { + "name": "threads_to_dashboards_added_by_fkey", + "tableFrom": "threads_to_dashboards", + "columnsFrom": [ + "added_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "threads_to_dashboards_pkey": { + "name": "threads_to_dashboards_pkey", + "columns": [ + "thread_id", + "dashboard_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_favorites": { + "name": "user_favorites", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_type": { + "name": "asset_type", + "type": "asset_type_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "order_index": { + "name": "order_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_favorites_user_id_fkey": { + "name": "user_favorites_user_id_fkey", + "tableFrom": "user_favorites", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "user_favorites_pkey": { + "name": "user_favorites_pkey", + "columns": [ + "user_id", + "asset_id", + "asset_type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_key": { + "name": "users_email_key", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_to_organizations": { + "name": "users_to_organizations", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "user_organization_role_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'querier'" + }, + "sharing_setting": { + "name": "sharing_setting", + "type": "sharing_setting_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "edit_sql": { + "name": "edit_sql", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "upload_csv": { + "name": "upload_csv", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "export_assets": { + "name": "export_assets", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_slack_enabled": { + "name": "email_slack_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deleted_by": { + "name": "deleted_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "user_organization_status_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_organizations_organization_id_fkey": { + "name": "users_to_organizations_organization_id_fkey", + "tableFrom": "users_to_organizations", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organizations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "users_to_organizations_user_id_fkey": { + "name": "users_to_organizations_user_id_fkey", + "tableFrom": "users_to_organizations", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "users_to_organizations_created_by_fkey": { + "name": "users_to_organizations_created_by_fkey", + "tableFrom": "users_to_organizations", + "columnsFrom": [ + "created_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "users_to_organizations_updated_by_fkey": { + "name": "users_to_organizations_updated_by_fkey", + "tableFrom": "users_to_organizations", + "columnsFrom": [ + "updated_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + }, + "users_to_organizations_deleted_by_fkey": { + "name": "users_to_organizations_deleted_by_fkey", + "tableFrom": "users_to_organizations", + "columnsFrom": [ + "deleted_by" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_organizations_pkey": { + "name": "users_to_organizations_pkey", + "columns": [ + "user_id", + "organization_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.asset_permission_role_enum": { + "name": "asset_permission_role_enum", + "schema": "public", + "values": [ + "owner", + "viewer", + "full_access", + "can_edit", + "can_filter", + "can_view" + ] + }, + "public.asset_type_enum": { + "name": "asset_type_enum", + "schema": "public", + "values": [ + "dashboard", + "thread", + "collection", + "chat", + "metric_file", + "dashboard_file" + ] + }, + "public.data_source_onboarding_status_enum": { + "name": "data_source_onboarding_status_enum", + "schema": "public", + "values": [ + "notStarted", + "inProgress", + "completed", + "failed" + ] + }, + "public.dataset_type_enum": { + "name": "dataset_type_enum", + "schema": "public", + "values": [ + "table", + "view", + "materializedView" + ] + }, + "public.identity_type_enum": { + "name": "identity_type_enum", + "schema": "public", + "values": [ + "user", + "team", + "organization" + ] + }, + "public.message_feedback_enum": { + "name": "message_feedback_enum", + "schema": "public", + "values": [ + "positive", + "negative" + ] + }, + "public.sharing_setting_enum": { + "name": "sharing_setting_enum", + "schema": "public", + "values": [ + "none", + "team", + "organization", + "public" + ] + }, + "public.slack_integration_status_enum": { + "name": "slack_integration_status_enum", + "schema": "public", + "values": [ + "pending", + "active", + "failed", + "revoked" + ] + }, + "public.stored_values_status_enum": { + "name": "stored_values_status_enum", + "schema": "public", + "values": [ + "syncing", + "success", + "failed" + ] + }, + "public.table_type_enum": { + "name": "table_type_enum", + "schema": "public", + "values": [ + "TABLE", + "VIEW", + "MATERIALIZED_VIEW", + "EXTERNAL_TABLE", + "TEMPORARY_TABLE" + ] + }, + "public.team_role_enum": { + "name": "team_role_enum", + "schema": "public", + "values": [ + "manager", + "member" + ] + }, + "public.user_organization_role_enum": { + "name": "user_organization_role_enum", + "schema": "public", + "values": [ + "workspace_admin", + "data_admin", + "querier", + "restricted_querier", + "viewer" + ] + }, + "public.user_organization_status_enum": { + "name": "user_organization_status_enum", + "schema": "public", + "values": [ + "active", + "inactive", + "pending", + "guest" + ] + }, + "public.verification_enum": { + "name": "verification_enum", + "schema": "public", + "values": [ + "verified", + "backlogged", + "inReview", + "requested", + "notRequested" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/database/drizzle/meta/_journal.json b/packages/database/drizzle/meta/_journal.json index ffb8d466d..0b04e071a 100644 --- a/packages/database/drizzle/meta/_journal.json +++ b/packages/database/drizzle/meta/_journal.json @@ -540,6 +540,13 @@ "when": 1752091186136, "tag": "0076_tired_madripoor", "breakpoints": true + }, + { + "idx": 77, + "version": "7", + "when": 1752150366202, + "tag": "0077_short_marvex", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/server-shared/src/security/requests.ts b/packages/server-shared/src/security/requests.ts index fb7e60a46..7bc42dea5 100644 --- a/packages/server-shared/src/security/requests.ts +++ b/packages/server-shared/src/security/requests.ts @@ -43,18 +43,7 @@ export const UpdateWorkspaceSettingsRequestSchema = z.object({ restrict_new_user_invitations: z.boolean().optional(), default_role: OrganizationRoleSchema.optional(), // this can either be a uuid or "all" - default_datasets_ids: z - .array( - z.union([ - z - .string() - .regex( - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ - ), - z.literal('all'), - ]) - ) - .optional(), + default_datasets_ids: z.array(z.union([z.uuid(), z.literal('all')])).optional(), }); export type UpdateWorkspaceSettingsRequest = z.infer;