diff --git a/apps/sim/app/api/cron/billing-cycle-close/route.ts b/apps/sim/app/api/cron/billing-cycle-close/route.ts new file mode 100644 index 00000000000..9eddbb212ee --- /dev/null +++ b/apps/sim/app/api/cron/billing-cycle-close/route.ts @@ -0,0 +1,57 @@ +import { createLogger } from '@sim/logger' +import { generateShortId } from '@sim/utils/id' +import { type NextRequest, NextResponse } from 'next/server' +import { verifyCronAuth } from '@/lib/auth/internal' +import { sweepBillingCycleCloses } from '@/lib/billing/cycle-close' +import { acquireLock, releaseLock } from '@/lib/core/config/redis' +import { runDetached } from '@/lib/core/utils/background' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('BillingCycleCloseCron') + +const LOCK_KEY = 'billing-cycle-close-lock' +/** Lock TTL in seconds — generous enough to cover the full sweep. */ +const LOCK_TTL_SECONDS = 15 * 60 + +export const dynamic = 'force-dynamic' + +/** + * Cron endpoint that closes elapsed billing periods (final overage collection, + * `billedOverageThisPeriod` reset, last-period bookkeeping). Configured in + * helm/sim/values.yaml under cronjobs.jobs.billingCycleClose. + * + * Acknowledges the cron call immediately and sweeps in the background; a Redis + * lock prevents overlapping runs, and each subscription's close is durably + * marked (`subscription.last_closed_period_start`), so replays are no-ops. + */ +export const GET = withRouteHandler(async (request: NextRequest) => { + const authError = verifyCronAuth(request, 'Billing cycle close') + if (authError) { + return authError + } + + const lockValue = generateShortId() + const locked = await acquireLock(LOCK_KEY, lockValue, LOCK_TTL_SECONDS, { + reclaimOnFailure: true, + }) + if (!locked) { + return NextResponse.json( + { success: true, message: 'Cycle-close sweep already in progress – skipped', status: 'skip' }, + { status: 202 } + ) + } + + runDetached('billing-cycle-close', async () => { + try { + const summary = await sweepBillingCycleCloses() + logger.info('Billing cycle-close sweep completed', { ...summary }) + } finally { + await releaseLock(LOCK_KEY, lockValue).catch(() => {}) + } + }) + + return NextResponse.json( + { success: true, message: 'Billing cycle-close sweep started', status: 'started' }, + { status: 202 } + ) +}) diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts index 96942b68a89..1afe2fb1731 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts @@ -89,7 +89,6 @@ export const GET = withRouteHandler( if (includeUsage && hasAdminAccess) { const usageData = await db .select({ - currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, usageLimitUpdatedAt: userStats.usageLimitUpdatedAt, lastPeriodCost: userStats.lastPeriodCost, @@ -99,20 +98,19 @@ export const GET = withRouteHandler( .limit(1) if (usageData.length > 0) { - const { billingPeriod, includeLegacyBaseline, usageByUser } = - await getOrganizationMemberUsageSnapshot(organizationId, { + const { billingPeriod, usageByUser } = await getOrganizationMemberUsageSnapshot( + organizationId, + { executor: dbReplica, userIds: [memberId], - }) + } + ) const memberLedger = usageByUser.get(memberId) ?? 0 memberData = { ...memberData, usage: { ...usageData[0], - currentPeriodCost: ( - (includeLegacyBaseline ? Number(usageData[0].currentPeriodCost ?? 0) : 0) + - memberLedger - ).toString(), + currentPeriodCost: memberLedger.toString(), billingPeriodStart: billingPeriod?.start ?? null, billingPeriodEnd: billingPeriod?.end ?? null, }, diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index 9f146e1b9b8..894c0204a98 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -101,7 +101,6 @@ export const GET = withRouteHandler( createdAt: member.createdAt, userName: user.name, userEmail: user.email, - currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, usageLimitUpdatedAt: userStats.usageLimitUpdatedAt, }) @@ -115,19 +114,18 @@ export const GET = withRouteHandler( totalQuery, ]) - const { billingPeriod, includeLegacyBaseline, usageByUser } = - await getOrganizationMemberUsageSnapshot(organizationId, { + const { billingPeriod, usageByUser } = await getOrganizationMemberUsageSnapshot( + organizationId, + { userIds: base.map((row) => row.userId), - }) + } + ) const billingPeriodStart = billingPeriod?.start ?? null const billingPeriodEnd = billingPeriod?.end ?? null const membersWithUsage = base.map((row) => ({ ...row, - currentPeriodCost: ( - (includeLegacyBaseline ? Number(row.currentPeriodCost ?? 0) : 0) + - (usageByUser.get(row.userId) ?? 0) - ).toString(), + currentPeriodCost: (usageByUser.get(row.userId) ?? 0).toString(), billingPeriodStart, billingPeriodEnd, })) diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts index 05bb34a6a59..779e14455b2 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts @@ -90,7 +90,6 @@ export const GET = withRouteHandler( createdAt: member.createdAt, userName: user.name, userEmail: user.email, - currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, billingBlocked: userStats.billingBlocked, }) @@ -104,10 +103,9 @@ export const GET = withRouteHandler( return notFoundResponse('Member') } - const { includeLegacyBaseline, usageByUser } = await getOrganizationMemberUsageSnapshot( - organizationId, - { userIds: [memberData.userId] } - ) + const { usageByUser } = await getOrganizationMemberUsageSnapshot(organizationId, { + userIds: [memberData.userId], + }) const data: AdminMemberDetail = { id: memberData.id, @@ -117,10 +115,7 @@ export const GET = withRouteHandler( createdAt: memberData.createdAt.toISOString(), userName: memberData.userName, userEmail: memberData.userEmail, - currentPeriodCost: ( - (includeLegacyBaseline ? Number(memberData.currentPeriodCost ?? 0) : 0) + - (usageByUser.get(memberData.userId) ?? 0) - ).toString(), + currentPeriodCost: (usageByUser.get(memberData.userId) ?? 0).toString(), currentUsageLimit: memberData.currentUsageLimit, billingBlocked: memberData.billingBlocked ?? false, } diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts index 6a7c1c17302..3ffe8289737 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts @@ -118,7 +118,6 @@ export const GET = withRouteHandler( createdAt: member.createdAt, userName: user.name, userEmail: user.email, - currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, billingBlocked: userStats.billingBlocked, }) @@ -133,12 +132,9 @@ export const GET = withRouteHandler( const total = countResult[0].count - const { includeLegacyBaseline, usageByUser } = await getOrganizationMemberUsageSnapshot( - organizationId, - { - userIds: membersData.map((row) => row.userId), - } - ) + const { usageByUser } = await getOrganizationMemberUsageSnapshot(organizationId, { + userIds: membersData.map((row) => row.userId), + }) const data: AdminMemberDetail[] = membersData.map((m) => ({ id: m.id, @@ -148,10 +144,7 @@ export const GET = withRouteHandler( createdAt: m.createdAt.toISOString(), userName: m.userName, userEmail: m.userEmail, - currentPeriodCost: ( - (includeLegacyBaseline ? Number(m.currentPeriodCost ?? 0) : 0) + - (usageByUser.get(m.userId) ?? 0) - ).toString(), + currentPeriodCost: (usageByUser.get(m.userId) ?? 0).toString(), currentUsageLimit: m.currentUsageLimit, billingBlocked: m.billingBlocked ?? false, })) diff --git a/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts b/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts index 85a5f69b63a..7e9de1a4935 100644 --- a/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts +++ b/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts @@ -12,7 +12,8 @@ * Body: * - currentUsageLimit?: number | null - Usage limit (null to use default) * - billingBlocked?: boolean - Block/unblock billing - * - currentPeriodCost?: number - Reset/adjust current period cost (use with caution) + * - currentPeriodCost?: number - Deprecated no-op: usage is the attributed + * usage_log ledger and cannot be adjusted here * - reason?: string - Reason for the change (for audit logging) * * Response: AdminSingleResponse<{ success: true, updated: string[], warnings: string[] }> @@ -79,9 +80,8 @@ export const GET = withRouteHandler( const [stats] = await db.select().from(userStats).where(eq(userStats.userId, userId)).limit(1) - // currentPeriodCost is now only a baseline; canonical current-period usage - // (baseline + attributed usage_log, refresh-adjusted) comes from the same - // helper users see, so admin reflects real usage instead of a stale 0. + // Canonical current-period usage (attributed usage_log, refresh-adjusted) + // comes from the same helper users see. const usage = await getUserUsageData(userId) const memberOrgs = await db @@ -197,10 +197,10 @@ export const PATCH = withRouteHandler( if (currentUsageLimit === null) { updateData.currentUsageLimit = null } else { - const currentCost = Number.parseFloat(existingStats?.currentPeriodCost || '0') - if (currentUsageLimit < currentCost) { + const { currentUsage } = await getUserUsageData(userId) + if (currentUsageLimit < currentUsage) { warnings.push( - `New limit ($${currentUsageLimit.toFixed(2)}) is below current usage ($${currentCost.toFixed(2)}). User may be immediately blocked.` + `New limit ($${currentUsageLimit.toFixed(2)}) is below current usage ($${currentUsage.toFixed(2)}). User may be immediately blocked.` ) } updateData.currentUsageLimit = currentUsageLimit.toFixed(2) @@ -225,13 +225,9 @@ export const PATCH = withRouteHandler( } if (currentPeriodCost !== undefined) { - const previousCost = existingStats?.currentPeriodCost || '0' warnings.push( - `Manually adjusting currentPeriodCost from $${previousCost} to $${currentPeriodCost.toFixed(2)}. This may affect billing accuracy.` + 'currentPeriodCost adjustments are deprecated: usage is the attributed usage_log ledger and cannot be edited here. The field was ignored.' ) - - updateData.currentPeriodCost = currentPeriodCost.toFixed(2) - updated.push('currentPeriodCost') } if (updated.length === 0) { @@ -256,7 +252,6 @@ export const PATCH = withRouteHandler( ? { currentUsageLimit: existingStats.currentUsageLimit, billingBlocked: existingStats.billingBlocked, - currentPeriodCost: existingStats.currentPeriodCost, } : null, newValues: updateData, diff --git a/apps/sim/lib/admin/dashboard-organizations.test.ts b/apps/sim/lib/admin/dashboard-organizations.test.ts index 49de9707c93..6f7a49f841c 100644 --- a/apps/sim/lib/admin/dashboard-organizations.test.ts +++ b/apps/sim/lib/admin/dashboard-organizations.test.ts @@ -291,13 +291,13 @@ describe('listDashboardOrganizations', () => { externalCollaboratorCount: 0, planLabel: 'No plan', }) - // Pagination, membership/collaborators, and two batched usage aggregates. + // Pagination, membership/collaborators, and the batched ledger aggregate. // This count remains constant regardless of the number of organizations. - expect(dbChainMockFns.select).toHaveBeenCalledTimes(6) + expect(dbChainMockFns.select).toHaveBeenCalledTimes(5) expect(dbChainMockFns.selectDistinctOn).toHaveBeenCalledTimes(1) }) - it('preserves the frozen baseline for an Enterprise subscription using its Stripe period', async () => { + it('reports ledger usage for an Enterprise subscription using its Stripe period', async () => { queueTableRows(organization, [{ total: 1 }]) queueTableRows(organization, [ { id: 'org-1', name: 'One', orgUsageLimit: '100', creditBalance: '0' }, @@ -325,13 +325,12 @@ describe('listDashboardOrganizations', () => { }, ]) queueTableRows(usageLog, [{ organizationId: 'org-1', cost: '2.5', workflowRuns: 3 }]) - queueTableRows(member, [{ organizationId: 'org-1', cost: '1.5' }]) const result = await listDashboardOrganizations({ search: '', limit: 50, offset: 0 }) expect(result.data[0]).toMatchObject({ reportingPeriod: { source: 'stripe' }, - usage: { usedDollars: 4, workflowRuns: 3 }, + usage: { usedDollars: 2.5, workflowRuns: 3 }, }) }) diff --git a/apps/sim/lib/admin/dashboard.ts b/apps/sim/lib/admin/dashboard.ts index 144fe0def4d..bdb62e65b72 100644 --- a/apps/sim/lib/admin/dashboard.ts +++ b/apps/sim/lib/admin/dashboard.ts @@ -254,24 +254,6 @@ async function getDashboardOrganizationUsage( } } - const legacyOrganizationIds = contexts - .filter((context) => context.period.source !== 'reporting') - .map((context) => context.organizationId) - if (legacyOrganizationIds.length > 0) { - const baselineTotals = await db - .select({ - organizationId: member.organizationId, - cost: sql`coalesce(sum(${userStats.currentPeriodCost}), 0)`, - }) - .from(member) - .leftJoin(userStats, eq(userStats.userId, member.userId)) - .where(inArray(member.organizationId, legacyOrganizationIds)) - .groupBy(member.organizationId) - for (const row of baselineTotals) { - const usage = result.get(row.organizationId) - if (usage) usage.total += Number(row.cost) - } - } return result } @@ -307,34 +289,6 @@ async function getDashboardOrganizationUsage( ) } - const legacyOrganizationIds = contexts - .filter((context) => context.period.source !== 'reporting') - .map((context) => context.organizationId) - if (legacyOrganizationIds.length > 0) { - const baselineRows = await db - .select({ - organizationId: member.organizationId, - userId: member.userId, - cost: userStats.currentPeriodCost, - }) - .from(member) - .leftJoin(userStats, eq(userStats.userId, member.userId)) - .where( - options.userIds - ? and( - inArray(member.organizationId, legacyOrganizationIds), - inArray(member.userId, options.userIds) - ) - : inArray(member.organizationId, legacyOrganizationIds) - ) - for (const row of baselineRows) { - const usage = result.get(row.organizationId) - if (!usage) continue - const amount = Number(row.cost ?? 0) - usage.total += amount - usage.byUser.set(row.userId, (usage.byUser.get(row.userId) ?? 0) + amount) - } - } return result } @@ -661,22 +615,6 @@ export async function listDashboardUsers({ search, limit, offset }: PaginationIn : [] ) ) - const legacyPersonalIds = personalUserIds.filter( - (userId) => personalPeriods.get(userId)?.source !== 'reporting' - ) - if (legacyPersonalIds.length > 0) { - const baselineRows = await db - .select({ userId: userStats.userId, cost: userStats.currentPeriodCost }) - .from(userStats) - .where(inArray(userStats.userId, legacyPersonalIds)) - for (const row of baselineRows) { - const current = personalUsage.get(row.userId) ?? { dollars: 0, workflowRuns: 0 } - personalUsage.set(row.userId, { - ...current, - dollars: current.dollars + Number(row.cost ?? 0), - }) - } - } return { data: rows.map((row) => { diff --git a/apps/sim/lib/api/contracts/v1/admin/organizations.ts b/apps/sim/lib/api/contracts/v1/admin/organizations.ts index af86a5f40c1..434daa41a9b 100644 --- a/apps/sim/lib/api/contracts/v1/admin/organizations.ts +++ b/apps/sim/lib/api/contracts/v1/admin/organizations.ts @@ -137,6 +137,7 @@ export const adminV1TransferOwnershipBodySchema = z.object({ const adminV1OrganizationMemberMutationResultSchema = adminV1MemberSchema.extend({ action: z.enum(['created', 'updated', 'already_member']), billingActions: z.object({ + /** @deprecated Always false — ledger entity stamps replaced join-time snapshots. */ proUsageSnapshotted: z.boolean(), proCancelledAtPeriodEnd: z.boolean(), }), @@ -147,9 +148,10 @@ const adminV1RemoveOrganizationMemberResultSchema = z.object({ memberId: z.string(), userId: z.string(), billingActions: z.object({ - /** Dollar amount of departed-member usage captured (0 when none). */ + /** @deprecated Always 0 — a departed member's ledger rows stay stamped to the org's period. */ usageCaptured: z.number(), proRestored: z.boolean(), + /** @deprecated Always false — no snapshot exists to restore. */ usageRestored: z.boolean(), skipBillingLogic: z.boolean(), }), diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 2f65fc1bbf5..152a8354199 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -70,7 +70,6 @@ import { handleAbandonedCheckout } from '@/lib/billing/webhooks/checkout' import { handleChargeDispute, handleDisputeClosed } from '@/lib/billing/webhooks/disputes' import { handleManualEnterpriseSubscription } from '@/lib/billing/webhooks/enterprise' import { - handleInvoiceFinalized, handleInvoicePaymentFailed, handleInvoicePaymentSucceeded, } from '@/lib/billing/webhooks/invoices' @@ -1502,10 +1501,6 @@ export const auth = betterAuth({ await handleInvoicePaymentFailed(event) break } - case 'invoice.finalized': { - await handleInvoiceFinalized(event) - break - } case 'customer.subscription.created': case 'customer.subscription.updated': { await handleManualEnterpriseSubscription(event) diff --git a/apps/sim/lib/billing/calculations/usage-monitor.test.ts b/apps/sim/lib/billing/calculations/usage-monitor.test.ts index bbd0a7a0a78..a11905e5980 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.test.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.test.ts @@ -8,20 +8,16 @@ const { mockGetBillingPeriodUsageCost, mockGetOrgMemberUsageForBillingPeriod, mockGetOrgMemberUsageLimit, - mockGetPooledOrgCurrentPeriodCost, mockGetUserUsageLimit, mockIsOrganizationBillingBlocked, mockComputeBillingPeriodUsageWithDailyRefresh, - mockGetOrgMemberRefreshBounds, } = vi.hoisted(() => ({ mockGetBillingPeriodUsageCost: vi.fn(), mockGetOrgMemberUsageForBillingPeriod: vi.fn(), mockGetOrgMemberUsageLimit: vi.fn(), - mockGetPooledOrgCurrentPeriodCost: vi.fn(), mockGetUserUsageLimit: vi.fn(), mockIsOrganizationBillingBlocked: vi.fn(), mockComputeBillingPeriodUsageWithDailyRefresh: vi.fn(), - mockGetOrgMemberRefreshBounds: vi.fn(), })) vi.mock('@/lib/billing/organizations/member-limits', () => ({ @@ -33,10 +29,9 @@ vi.mock('@/lib/billing/core/access', () => ({ isOrganizationBillingBlocked: mockIsOrganizationBillingBlocked, })) -// core/usage pulls in the email-rendering chain at import; stub the two symbols +// core/usage pulls in the email-rendering chain at import; stub the symbol // usage-monitor imports from it so the module loads in a node test env. vi.mock('@/lib/billing/core/usage', () => ({ - getPooledOrgCurrentPeriodCost: mockGetPooledOrgCurrentPeriodCost, getUserUsageLimit: mockGetUserUsageLimit, })) @@ -46,13 +41,13 @@ vi.mock('@/lib/billing/core/usage-log', () => ({ vi.mock('@/lib/billing/credits/daily-refresh', () => ({ computeBillingPeriodUsageWithDailyRefresh: mockComputeBillingPeriodUsageWithDailyRefresh, - getOrgMemberRefreshBounds: mockGetOrgMemberRefreshBounds, })) import { checkBillingBlocked, checkBillingEntityBlocked, checkOrganizationMemberUsageLimit, + checkServerSideUsageLimits, checkUsageStatus, } from '@/lib/billing/calculations/usage-monitor' @@ -73,7 +68,6 @@ describe('checkUsageStatus', () => { ledgerUsage: 125, refreshConsumed: 25, }) - mockGetOrgMemberRefreshBounds.mockResolvedValue({}) }) it('reads reporting-period organization usage without loading the member roster', async () => { @@ -109,7 +103,6 @@ describe('checkUsageStatus', () => { { type: 'organization', id: 'org-1' }, billingPeriod ) - expect(mockGetPooledOrgCurrentPeriodCost).not.toHaveBeenCalled() }) it('reads paid personal ledger usage and refresh from one snapshot', async () => { @@ -123,17 +116,14 @@ describe('checkUsageStatus', () => { periodStart, periodEnd, } - dbChainMockFns.limit.mockResolvedValueOnce([{ currentPeriodCost: '20' }]) - await expect(checkUsageStatus('user-1', subscription)).resolves.toMatchObject({ - currentUsage: 120, + currentUsage: 100, scope: 'user', }) expect(mockComputeBillingPeriodUsageWithDailyRefresh).toHaveBeenCalledWith({ billingEntity: { type: 'user', id: 'user-1' }, billingPeriod: { start: periodStart, end: periodEnd }, - userIds: ['user-1'], refreshPeriodStart: periodStart, refreshPeriodEnd: periodEnd, planDollars: 20, @@ -152,7 +142,6 @@ describe('checkUsageStatus', () => { periodStart, periodEnd, } - dbChainMockFns.limit.mockResolvedValueOnce([{ currentPeriodCost: '0' }]) mockComputeBillingPeriodUsageWithDailyRefresh.mockResolvedValueOnce({ ledgerUsage: -1, refreshConsumed: 1, @@ -175,10 +164,8 @@ describe('checkUsageStatus', () => { periodStart, periodEnd, } - dbChainMockFns.limit.mockResolvedValueOnce([{ currentPeriodCost: '20' }]) - await expect(checkUsageStatus('user-1', subscription)).resolves.toMatchObject({ - currentUsage: 145, + currentUsage: 125, scope: 'user', }) @@ -200,7 +187,6 @@ describe('checkUsageStatus', () => { periodStart, periodEnd, } - dbChainMockFns.limit.mockResolvedValueOnce([{ currentPeriodCost: '0' }]) mockGetBillingPeriodUsageCost.mockResolvedValueOnce(-1) await expect(checkUsageStatus('user-1', subscription)).resolves.toMatchObject({ @@ -211,10 +197,9 @@ describe('checkUsageStatus', () => { expect(mockComputeBillingPeriodUsageWithDailyRefresh).not.toHaveBeenCalled() }) - it('combines paid organization ledger usage with bounded member refresh', async () => { + it('combines paid organization ledger usage with entity-scoped refresh — no roster read', async () => { const periodStart = new Date('2026-06-01T00:00:00.000Z') const periodEnd = new Date('2026-07-01T00:00:00.000Z') - const userStart = new Date('2026-06-10T00:00:00.000Z') const subscription = { referenceId: 'org-1', plan: 'team', @@ -223,22 +208,19 @@ describe('checkUsageStatus', () => { periodStart, periodEnd, } - mockGetPooledOrgCurrentPeriodCost.mockResolvedValue({ - memberIds: ['user-1', 'user-2'], - currentPeriodCost: 20, - }) - mockGetOrgMemberRefreshBounds.mockResolvedValue({ 'user-2': { userStart } }) mockComputeBillingPeriodUsageWithDailyRefresh.mockResolvedValue({ ledgerUsage: 100, refreshConsumed: 10, }) await expect(checkUsageStatus('user-1', subscription)).resolves.toMatchObject({ - currentUsage: 110, + currentUsage: 90, scope: 'organization', organizationId: 'org-1', }) + // Refresh is scoped by the entity stamps alone, so departed members' + // org-attributed rows participate identically to current members'. expect(mockComputeBillingPeriodUsageWithDailyRefresh).toHaveBeenCalledWith({ billingEntity: { type: 'organization', id: 'org-1' }, billingPeriod: expect.objectContaining({ @@ -246,37 +228,45 @@ describe('checkUsageStatus', () => { end: periodEnd, source: 'stripe', }), - userIds: ['user-1', 'user-2'], refreshPeriodStart: periodStart, refreshPeriodEnd: periodEnd, planDollars: expect.any(Number), seats: 2, - userBounds: { 'user-2': { userStart } }, }) expect(mockGetBillingPeriodUsageCost).not.toHaveBeenCalled() }) +}) - it('returns ledger usage without refresh when an organization has no members', async () => { - const periodStart = new Date('2026-06-01T00:00:00.000Z') - const periodEnd = new Date('2026-07-01T00:00:00.000Z') +describe('checkServerSideUsageLimits', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + setEnvFlags({ isHosted: true, isBillingEnabled: true }) + mockGetBillingPeriodUsageCost.mockResolvedValue(125) + }) + + it('keeps blocked accounts blocked while reporting their real ledger usage', async () => { + dbChainMockFns.limit.mockResolvedValueOnce([{ blocked: true, blockedReason: 'payment_failed' }]) const subscription = { - referenceId: 'org-1', - plan: 'team', + referenceId: 'user-1', + plan: 'pro', status: 'active', seats: 1, - periodStart, - periodEnd, + periodStart: new Date('2026-06-01T00:00:00.000Z'), + periodEnd: new Date('2026-07-01T00:00:00.000Z'), } - mockGetPooledOrgCurrentPeriodCost.mockResolvedValue({ memberIds: [], currentPeriodCost: 0 }) - await expect(checkUsageStatus('user-1', subscription)).resolves.toMatchObject({ - currentUsage: 125, - scope: 'organization', - }) + const result = await checkServerSideUsageLimits('user-1', subscription) - expect(mockGetBillingPeriodUsageCost).toHaveBeenCalledTimes(1) - expect(mockComputeBillingPeriodUsageWithDailyRefresh).not.toHaveBeenCalled() - expect(mockGetOrgMemberRefreshBounds).not.toHaveBeenCalled() + expect(result).toMatchObject({ isExceeded: true, currentUsage: 125, limit: 0 }) + expect(result.message).toBeTruthy() + expect(mockGetBillingPeriodUsageCost).toHaveBeenCalledWith( + { type: 'user', id: 'user-1' }, + expect.objectContaining({ + start: subscription.periodStart, + end: subscription.periodEnd, + }) + ) }) }) diff --git a/apps/sim/lib/billing/calculations/usage-monitor.ts b/apps/sim/lib/billing/calculations/usage-monitor.ts index c9b84671a12..b8387fd28e0 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.ts @@ -7,11 +7,7 @@ import { isOrganizationBillingBlocked } from '@/lib/billing/core/access' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' import { resolveSubscriptionUsagePeriod } from '@/lib/billing/core/reporting-period' -import { - getPooledOrgCurrentPeriodCost, - getUserUsageLimit, - type UsageLimitSubscription, -} from '@/lib/billing/core/usage' +import { getUserUsageLimit, type UsageLimitSubscription } from '@/lib/billing/core/usage' import { type BillingContext, type BillingEntity, @@ -19,17 +15,13 @@ import { type UsageQueryPeriod, } from '@/lib/billing/core/usage-log' import { dollarsToCredits } from '@/lib/billing/credits/conversion' -import { - computeBillingPeriodUsageWithDailyRefresh, - getOrgMemberRefreshBounds, -} from '@/lib/billing/credits/daily-refresh' +import { computeBillingPeriodUsageWithDailyRefresh } from '@/lib/billing/credits/daily-refresh' import { getOrgMemberUsageForBillingPeriod, getOrgMemberUsageLimit, } from '@/lib/billing/organizations/member-limits' import { getPlanTierDollars, isPaid } from '@/lib/billing/plan-helpers' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' -import { toDecimal, toNumber } from '@/lib/billing/utils/decimal' import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags' const logger = createLogger('UsageMonitor') @@ -64,49 +56,26 @@ async function computePooledOrgUsage( anchorDate: null, interval: null, } - if (billingPeriod.source === 'reporting') { - const ledgerUsage = await getBillingPeriodUsageCost( - { type: 'organization', id: organizationId }, - billingPeriod - ) - return ledgerUsage - } - - const { memberIds, currentPeriodCost } = await getPooledOrgCurrentPeriodCost(organizationId) - if (memberIds.length === 0) { - return getBillingPeriodUsageCost({ type: 'organization', id: organizationId }, billingPeriod) - } if (!isPaid(sub.plan) || !sub.periodStart) { - const ledgerUsage = await getBillingPeriodUsageCost( - { type: 'organization', id: organizationId }, - billingPeriod - ) - return currentPeriodCost + ledgerUsage + return getBillingPeriodUsageCost({ type: 'organization', id: organizationId }, billingPeriod) } const planDollars = getPlanTierDollars(sub.plan) if (planDollars <= 0) { - const ledgerUsage = await getBillingPeriodUsageCost( - { type: 'organization', id: organizationId }, - billingPeriod - ) - return currentPeriodCost + ledgerUsage + return getBillingPeriodUsageCost({ type: 'organization', id: organizationId }, billingPeriod) } - const userBounds = await getOrgMemberRefreshBounds(organizationId, sub.periodStart) const { ledgerUsage, refreshConsumed } = await computeBillingPeriodUsageWithDailyRefresh({ billingEntity: { type: 'organization', id: organizationId }, billingPeriod, - userIds: memberIds, refreshPeriodStart: sub.periodStart, refreshPeriodEnd: sub.periodEnd ?? null, planDollars, seats: sub.seats || 1, - userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined, }) - return Math.max(0, currentPeriodCost + ledgerUsage - refreshConsumed) + return Math.max(0, ledgerUsage - refreshConsumed) } /** @@ -120,9 +89,11 @@ export async function checkUsageStatus( ): Promise { try { if (!isBillingEnabled) { - const statsRecords = await db.select().from(userStats).where(eq(userStats.userId, userId)) - const currentUsage = - statsRecords.length > 0 ? toNumber(toDecimal(statsRecords[0].currentPeriodCost)) : 0 + // Self-hosted display: lifetime ledger over the open default window. + const currentUsage = await getBillingPeriodUsageCost( + { type: 'user', id: userId }, + { ...defaultBillingPeriod(), source: 'default' } + ) return { percentUsed: Math.min((currentUsage / 1000) * 100, 100), @@ -156,25 +127,6 @@ export async function checkUsageStatus( return buildUsageData({ currentUsage, limit, scope, organizationId }) } - const statsRecords = await db - .select() - .from(userStats) - .where(eq(userStats.userId, userId)) - .limit(1) - - if (statsRecords.length === 0) { - logger.info('No usage stats found for user', { userId, limit }) - return { - percentUsed: 0, - isWarning: false, - isExceeded: false, - currentUsage: 0, - limit, - scope: 'user', - organizationId: null, - } - } - const billingPeriod = preloadedBillingContext?.billingPeriod ?? (sub?.periodStart && sub.periodEnd @@ -189,7 +141,6 @@ export async function checkUsageStatus( const usage = await computeBillingPeriodUsageWithDailyRefresh({ billingEntity: { type: 'user', id: userId }, billingPeriod, - userIds: [userId], refreshPeriodStart: sub.periodStart, refreshPeriodEnd: sub.periodEnd ?? null, planDollars, @@ -203,8 +154,7 @@ export async function checkUsageStatus( } else { ledgerUsage = await getBillingPeriodUsageCost({ type: 'user', id: userId }, billingPeriod) } - const usageBeforeRefresh = - toNumber(toDecimal(statsRecords[0].currentPeriodCost)) + ledgerUsage - refreshConsumed + const usageBeforeRefresh = ledgerUsage - refreshConsumed const currentUsage = appliedDailyRefresh ? Math.max(0, usageBeforeRefresh) : usageBeforeRefresh return buildUsageData({ currentUsage, limit, scope, organizationId }) @@ -364,16 +314,22 @@ export async function checkServerSideUsageLimits( logger.info('Server-side checking usage limits for user', { userId }) - const stats = await db - .select({ current: userStats.currentPeriodCost }) - .from(userStats) - .where(eq(userStats.userId, userId)) - .limit(1) - - const currentUsage = stats.length > 0 ? toNumber(toDecimal(stats[0].current)) : 0 - const blocked = await checkBillingBlocked(userId) if (blocked.blocked) { + // Enforcement stays blocked, but surfaced usage must be the real ledger + // value — `/api/users/me/usage-limits` exposes it as `currentPeriodCost`. + const sub = + preloadedSubscription !== undefined + ? preloadedSubscription + : await getHighestPrioritySubscription(userId) + const subIsOrgScoped = isOrgScopedSubscription(sub, userId) + const billingEntity: BillingEntity = + subIsOrgScoped && sub + ? { type: 'organization', id: sub.referenceId } + : { type: 'user', id: userId } + const billingPeriod = preloadedBillingContext?.billingPeriod ?? + resolveSubscriptionUsagePeriod(sub) ?? { ...defaultBillingPeriod(), source: 'default' } + const currentUsage = await getBillingPeriodUsageCost(billingEntity, billingPeriod) return { isExceeded: true, currentUsage, limit: 0, message: blocked.message } } diff --git a/apps/sim/lib/billing/core/billing.test.ts b/apps/sim/lib/billing/core/billing.test.ts index ea431214e91..4ee6c354483 100644 --- a/apps/sim/lib/billing/core/billing.test.ts +++ b/apps/sim/lib/billing/core/billing.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { dbChainMock, dbChainMockFns } from '@sim/testing' +import { dbChainMock, dbChainMockFns, queueTableRows, schemaMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { @@ -42,18 +42,17 @@ vi.mock('@/lib/billing/core/usage-log', () => ({ vi.mock('@/lib/billing/credits/daily-refresh', () => ({ computeDailyRefreshConsumed: mockComputeDailyRefreshConsumed, - getOrgMemberRefreshBounds: vi.fn(), })) -import { getPersonalBillingSummary } from '@/lib/billing/core/billing' +import { calculateSubscriptionOverage, getPersonalBillingSummary } from '@/lib/billing/core/billing' describe('getPersonalBillingSummary', () => { beforeEach(() => { vi.clearAllMocks() mockEnsureUserStatsExists.mockResolvedValue(undefined) mockResolveBillingInterval.mockReturnValue('year') - mockComputeDailyRefreshConsumed.mockResolvedValue(3) - mockGetBillingPeriodUsageCostWithSourceSubset.mockResolvedValue({ total: 2, subset: 1 }) + mockComputeDailyRefreshConsumed.mockResolvedValue(1) + mockGetBillingPeriodUsageCostWithSourceSubset.mockResolvedValue({ total: 4, subset: 1 }) mockGetHighestPriorityPersonalSubscription.mockResolvedValue({ id: 'personal-sub', referenceId: 'viewer-a', @@ -74,12 +73,8 @@ describe('getPersonalBillingSummary', () => { }) dbChainMockFns.limit.mockResolvedValueOnce([ { - currentPeriodCost: '10', currentUsageLimit: '30', lastPeriodCost: '6', - proPeriodCostSnapshot: '4', - proPeriodCostSnapshotAt: new Date('2026-07-10T00:00:00.000Z'), - currentPeriodCopilotCost: '5', lastPeriodCopilotCost: '2', creditBalance: '7', billingBlocked: true, @@ -117,10 +112,50 @@ describe('getPersonalBillingSummary', () => { }) expect(mockComputeDailyRefreshConsumed).toHaveBeenCalledWith( expect.objectContaining({ - periodEnd: new Date('2026-07-10T00:00:00.000Z'), + periodEnd: new Date('2026-08-01T00:00:00.000Z'), billingEntity: { type: 'user', id: 'viewer-a' }, }), dbChainMock.db ) }) }) + +describe('calculateSubscriptionOverage', () => { + beforeEach(() => { + vi.clearAllMocks() + mockComputeDailyRefreshConsumed.mockResolvedValue(0) + }) + + it('bills the pooled org ledger with entity-scoped refresh — no roster read', async () => { + queueTableRows(schemaMock.organization, [{ id: 'org-1' }]) // isSubscriptionOrgScoped + // Pooled ledger sum includes departed members' org-stamped rows. + mockGetBillingPeriodUsageCost.mockResolvedValue(160) + + const overage = await calculateSubscriptionOverage({ + id: 'sub-1', + plan: 'team', + referenceId: 'org-1', + seats: 2, + periodStart: new Date('2026-07-01T00:00:00.000Z'), + periodEnd: new Date('2026-08-01T00:00:00.000Z'), + }) + + expect(mockGetBillingPeriodUsageCost).toHaveBeenCalledWith( + { type: 'organization', id: 'org-1' }, + { + start: new Date('2026-07-01T00:00:00.000Z'), + end: new Date('2026-08-01T00:00:00.000Z'), + } + ) + // Refresh is scoped by the same entity stamps as the ledger sum — no + // actor list, so departed members' rows participate identically. + expect(mockComputeDailyRefreshConsumed).toHaveBeenCalledWith({ + billingEntity: { type: 'organization', id: 'org-1' }, + periodStart: new Date('2026-07-01T00:00:00.000Z'), + periodEnd: new Date('2026-08-01T00:00:00.000Z'), + planDollars: 40, + seats: 2, + }) + expect(overage).toBe(80) + }) +}) diff --git a/apps/sim/lib/billing/core/billing.ts b/apps/sim/lib/billing/core/billing.ts index 8372293a0f9..ff0308e593c 100644 --- a/apps/sim/lib/billing/core/billing.ts +++ b/apps/sim/lib/billing/core/billing.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { member, organization, subscription, userStats } from '@sim/db/schema' +import { organization, subscription, userStats } from '@sim/db/schema' import { and, desc, eq, inArray } from 'drizzle-orm' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { @@ -12,10 +12,7 @@ import { getBillingPeriodUsageCost, getBillingPeriodUsageCostWithSourceSubset, } from '@/lib/billing/core/usage-log' -import { - computeDailyRefreshConsumed, - getOrgMemberRefreshBounds, -} from '@/lib/billing/credits/daily-refresh' +import { computeDailyRefreshConsumed } from '@/lib/billing/credits/daily-refresh' import { getPlanTierDollars, isEnterprise, isPaid, isPro, isTeam } from '@/lib/billing/plan-helpers' import { ENTITLED_SUBSCRIPTION_STATUSES, @@ -113,70 +110,11 @@ export async function isSubscriptionOrgScoped(sub: { referenceId: string }): Pro } /** - * Aggregate raw pooled stats for all members of an organization in a single - * query. Used by org-scoped summary and overage calculations so we don't - * call `getUserUsageData` per-member — that helper now returns the entire - * pool for org-scoped subs, which would N-times-count the usage. - * - * The `currentPeriodCost` sum here is semantically identical to - * `getPooledOrgCurrentPeriodCost` (same `LEFT JOIN` + `toDecimal` - * null handling); this helper bundles the copilot fields in the same - * round-trip. Never fall back to lifetime `totalCost` on nulls — the - * column is `NOT NULL DEFAULT '0'` and mixing scopes would break - * current-period billing math. - */ -async function aggregateOrgMemberStats( - organizationId: string, - executor: DbClient = db -): Promise<{ - memberIds: string[] - currentPeriodCost: number - currentPeriodCopilotCost: number - lastPeriodCopilotCost: number -}> { - const rows = await executor - .select({ - userId: member.userId, - currentPeriodCost: userStats.currentPeriodCost, - currentPeriodCopilotCost: userStats.currentPeriodCopilotCost, - lastPeriodCopilotCost: userStats.lastPeriodCopilotCost, - }) - .from(member) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, organizationId)) - - let currentPeriodCost = new Decimal(0) - // Copilot baseline (copilot source). All copilot-family usage (incl. MCP) lives - // in usage_log and is added via the copilot ledger by callers — not a baseline. - let currentPeriodCopilotCost = new Decimal(0) - let lastPeriodCopilotCost = new Decimal(0) - const memberIds: string[] = [] - - for (const row of rows) { - memberIds.push(row.userId) - currentPeriodCost = currentPeriodCost.plus(toDecimal(row.currentPeriodCost)) - currentPeriodCopilotCost = currentPeriodCopilotCost.plus( - toDecimal(row.currentPeriodCopilotCost) - ) - lastPeriodCopilotCost = lastPeriodCopilotCost.plus(toDecimal(row.lastPeriodCopilotCost)) - } - - return { - memberIds, - currentPeriodCost: toNumber(currentPeriodCost), - currentPeriodCopilotCost: toNumber(currentPeriodCopilotCost), - lastPeriodCopilotCost: toNumber(lastPeriodCopilotCost), - } -} - -/** - * Compute an org's overage amount from already-fetched pool/departed - * inputs. Internally performs one daily-refresh DB read to subtract - * refresh credits; callers are expected to have already loaded the - * pooled `currentPeriodCost` and `departedMemberUsage` (threshold - * billing passes lock-held values; `calculateSubscriptionOverage` - * passes lockless values from `aggregateOrgMemberStats`). Both - * callers route through this to keep the overage math in one place. + * Compute an org's overage amount from an already-fetched pooled ledger sum. + * Internally performs one daily-refresh DB read to subtract refresh credits; + * callers pass the org-attributed ledger usage for the period (threshold + * billing passes the current period; cycle close passes the closed period). + * All callers route through this to keep the overage math in one place. */ export async function computeOrgOverageAmount(params: { plan: string | null @@ -184,29 +122,24 @@ export async function computeOrgOverageAmount(params: { periodStart: Date | null periodEnd: Date | null organizationId: string - pooledCurrentPeriodCost: number - departedMemberUsage: number - memberIds: string[] + pooledLedgerUsage: number }): Promise<{ effectiveUsage: number baseSubscriptionAmount: number dailyRefreshDeduction: number totalOverage: number }> { - const totalUsage = params.pooledCurrentPeriodCost + params.departedMemberUsage + const totalUsage = params.pooledLedgerUsage let dailyRefreshDeduction = 0 const planDollars = getPlanTierDollars(params.plan) - if (planDollars > 0 && params.periodStart && params.memberIds.length > 0) { - const userBounds = await getOrgMemberRefreshBounds(params.organizationId, params.periodStart) + if (planDollars > 0 && params.periodStart) { dailyRefreshDeduction = await computeDailyRefreshConsumed({ - userIds: params.memberIds, + billingEntity: { type: 'organization', id: params.organizationId }, periodStart: params.periodStart, periodEnd: params.periodEnd ?? null, planDollars, seats: params.seats || 1, - userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined, - billingEntity: { type: 'organization', id: params.organizationId }, }) } @@ -244,7 +177,6 @@ export async function calculateSubscriptionOverage(sub: { const isOrgScoped = await isSubscriptionOrgScoped(sub) if (isOrgScoped) { - const pooled = await aggregateOrgMemberStats(sub.referenceId) const ledgerUsage = sub.periodStart && sub.periodEnd ? await getBillingPeriodUsageCost( @@ -253,24 +185,13 @@ export async function calculateSubscriptionOverage(sub: { ) : 0 - const orgData = await db - .select({ departedMemberUsage: organization.departedMemberUsage }) - .from(organization) - .where(eq(organization.id, sub.referenceId)) - .limit(1) - - const departedMemberUsage = - orgData.length > 0 ? toNumber(toDecimal(orgData[0].departedMemberUsage)) : 0 - const { totalOverage, effectiveUsage, baseSubscriptionAmount } = await computeOrgOverageAmount({ plan: sub.plan, seats: sub.seats ?? null, periodStart: sub.periodStart ?? null, periodEnd: sub.periodEnd ?? null, organizationId: sub.referenceId, - pooledCurrentPeriodCost: pooled.currentPeriodCost + ledgerUsage, - departedMemberUsage, - memberIds: pooled.memberIds, + pooledLedgerUsage: ledgerUsage, }) totalOverageDecimal = toDecimal(totalOverage) @@ -278,33 +199,18 @@ export async function calculateSubscriptionOverage(sub: { logger.info('Calculated org-scoped overage', { subscriptionId: sub.id, plan: sub.plan, - currentMemberUsage: pooled.currentPeriodCost + ledgerUsage, - departedMemberUsage, ledgerUsage, - totalUsage: pooled.currentPeriodCost + ledgerUsage + departedMemberUsage, effectiveUsage, baseSubscriptionAmount, totalOverage, }) - } else if (isPro(sub.plan)) { - // Read user_stats directly (not via `getUserUsageData`). Priority - // lookup prefers org over personal within tier, so during a - // cancel-at-period-end grace window it would return pooled org usage - // instead of this user's personal period — overbilling the final - // personal Pro invoice. - const [statsRow] = await db - .select({ - currentPeriodCost: userStats.currentPeriodCost, - proPeriodCostSnapshot: userStats.proPeriodCostSnapshot, - proPeriodCostSnapshotAt: userStats.proPeriodCostSnapshotAt, - }) - .from(userStats) - .where(eq(userStats.userId, sub.referenceId)) - .limit(1) - - const personalCurrentUsage = statsRow ? toNumber(toDecimal(statsRow.currentPeriodCost)) : 0 - const snapshotUsage = statsRow ? toNumber(toDecimal(statsRow.proPeriodCostSnapshot)) : 0 - const snapshotAt = statsRow?.proPeriodCostSnapshotAt ?? null + } else { + // Ledger sums are read for the exact reference user (not via + // `getUserUsageData`). Priority lookup prefers org over personal within + // tier, so during a cancel-at-period-end grace window it would return + // pooled org usage instead of this user's personal period — overbilling + // the final personal invoice. Ledger entity stamps already attribute + // post-org-join usage to the org, so the personal sum excludes it. const ledgerUsage = sub.periodStart && sub.periodEnd ? await getBillingPeriodUsageCost( @@ -313,82 +219,30 @@ export async function calculateSubscriptionOverage(sub: { ) : 0 - const joinedOrgMidCycle = snapshotAt !== null || snapshotUsage > 0 - const totalProUsageDecimal = joinedOrgMidCycle - ? toDecimal(snapshotUsage).plus(ledgerUsage) - : toDecimal(personalCurrentUsage).plus(ledgerUsage) - - if (joinedOrgMidCycle) { - logger.info('Billing personal Pro only for pre-join usage (user joined org mid-cycle)', { - userId: sub.referenceId, - preJoinUsage: snapshotUsage, - postJoinUsageOnMemberRow: personalCurrentUsage, - snapshotAt: snapshotAt?.toISOString() ?? null, - subscriptionId: sub.id, - }) - } - let dailyRefreshDeduction = 0 - const planDollars = getPlanTierDollars(sub.plan) - if (planDollars > 0 && sub.periodStart) { - // If the user joined an org mid-cycle, their usageLog rows after - // `snapshotAt` belong to the org's pooled refresh. Cap refresh - // to [periodStart, snapshotAt) so post-join refresh isn't - // deducted from pre-join personal Pro usage. - const refreshCap = joinedOrgMidCycle && snapshotAt ? snapshotAt : (sub.periodEnd ?? null) - dailyRefreshDeduction = await computeDailyRefreshConsumed({ - userIds: [sub.referenceId], - periodStart: sub.periodStart, - periodEnd: refreshCap, - planDollars, - billingEntity: { type: 'user', id: sub.referenceId }, - }) + if (isPro(sub.plan)) { + const planDollars = getPlanTierDollars(sub.plan) + if (planDollars > 0 && sub.periodStart) { + dailyRefreshDeduction = await computeDailyRefreshConsumed({ + billingEntity: { type: 'user', id: sub.referenceId }, + periodStart: sub.periodStart, + periodEnd: sub.periodEnd ?? null, + planDollars, + }) + } } - const effectiveUsageDecimal = Decimal.max( - 0, - totalProUsageDecimal.minus(toDecimal(dailyRefreshDeduction)) - ) - const { basePrice } = getPlanPricing(sub.plan ?? '') - totalOverageDecimal = Decimal.max(0, effectiveUsageDecimal.minus(basePrice)) - - logger.info('Calculated personal pro overage', { - subscriptionId: sub.id, - joinedOrgMidCycle, - personalCurrentUsage, - snapshot: snapshotUsage, - ledgerUsage, - billedUsage: toNumber(totalProUsageDecimal), - dailyRefreshDeduction, - basePrice, - totalOverage: toNumber(totalOverageDecimal), - }) - } else { - // Free or unknown plan. Same direct-read rationale as the Pro branch. - const [statsRow] = await db - .select({ currentPeriodCost: userStats.currentPeriodCost }) - .from(userStats) - .where(eq(userStats.userId, sub.referenceId)) - .limit(1) - const personalCurrentUsage = statsRow ? toNumber(toDecimal(statsRow.currentPeriodCost)) : 0 - const ledgerUsage = - sub.periodStart && sub.periodEnd - ? await getBillingPeriodUsageCost( - { type: 'user', id: sub.referenceId }, - { start: sub.periodStart, end: sub.periodEnd } - ) - : 0 const { basePrice } = getPlanPricing(sub.plan || 'free') totalOverageDecimal = Decimal.max( 0, - toDecimal(personalCurrentUsage).plus(ledgerUsage).minus(basePrice) + toDecimal(ledgerUsage).minus(toDecimal(dailyRefreshDeduction)).minus(basePrice) ) - logger.info('Calculated overage for plan', { + logger.info('Calculated personal overage', { subscriptionId: sub.id, plan: sub.plan || 'free', - usage: personalCurrentUsage + ledgerUsage, ledgerUsage, + dailyRefreshDeduction, basePrice, totalOverage: toNumber(totalOverageDecimal), }) @@ -410,12 +264,8 @@ export async function getPersonalBillingSummary(userId: string, executor: DbClie getHighestPriorityPersonalSubscription(userId, { executor }), db .select({ - currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, lastPeriodCost: userStats.lastPeriodCost, - proPeriodCostSnapshot: userStats.proPeriodCostSnapshot, - proPeriodCostSnapshotAt: userStats.proPeriodCostSnapshotAt, - currentPeriodCopilotCost: userStats.currentPeriodCopilotCost, lastPeriodCopilotCost: userStats.lastPeriodCopilotCost, creditBalance: userStats.creditBalance, billingBlocked: userStats.billingBlocked, @@ -444,12 +294,7 @@ export async function getPersonalBillingSummary(userId: string, executor: DbClie executor ) - const hasPersonalUsageSnapshot = - Boolean(personalSubscription) && isPro(plan) && stats.proPeriodCostSnapshotAt !== null - const personalUsageBaseline = hasPersonalUsageSnapshot - ? stats.proPeriodCostSnapshot - : stats.currentPeriodCost - const currentUsage = toDecimal(personalUsageBaseline).plus(ledgerUsage) + const currentUsage = toDecimal(ledgerUsage) let refreshDeduction = 0 if ( @@ -462,13 +307,10 @@ export async function getPersonalBillingSummary(userId: string, executor: DbClie if (planDollars > 0) { refreshDeduction = await computeDailyRefreshConsumed( { - userIds: [userId], + billingEntity: { type: 'user', id: userId }, periodStart: personalSubscription.periodStart, - periodEnd: hasPersonalUsageSnapshot - ? stats.proPeriodCostSnapshotAt - : (personalSubscription.periodEnd ?? null), + periodEnd: personalSubscription.periodEnd ?? null, planDollars, - billingEntity: { type: 'user', id: userId }, }, executor ) @@ -528,9 +370,7 @@ export async function getPersonalBillingSummary(userId: string, executor: DbClie lastPeriodCost: toNumber(toDecimal(stats.lastPeriodCost)), lastPeriodCopilotCost: toNumber(toDecimal(stats.lastPeriodCopilotCost)), daysRemaining, - copilotCost: - (hasPersonalUsageSnapshot ? 0 : toNumber(toDecimal(stats.currentPeriodCopilotCost))) + - copilotLedgerUsage, + copilotCost: copilotLedgerUsage, }, } } catch (error) { diff --git a/apps/sim/lib/billing/core/organization-usage.test.ts b/apps/sim/lib/billing/core/organization-usage.test.ts index fc65277219a..b1838a49664 100644 --- a/apps/sim/lib/billing/core/organization-usage.test.ts +++ b/apps/sim/lib/billing/core/organization-usage.test.ts @@ -28,7 +28,7 @@ describe('getOrganizationMemberUsageSnapshot', () => { afterEach(() => vi.useRealTimers()) - it('uses the Enterprise reporting window and excludes the legacy baseline', async () => { + it('uses the Enterprise reporting window for anchored organizations', async () => { getOrganizationSubscription.mockResolvedValue({ plan: 'enterprise', billingInterval: 'year', @@ -46,7 +46,6 @@ describe('getOrganizationMemberUsageSnapshot', () => { start: new Date('2026-01-01T00:00:00.000Z'), end: new Date('2027-01-01T00:00:00.000Z'), }) - expect(snapshot.includeLegacyBaseline).toBe(false) expect(getBillingPeriodUsageCostByUser).toHaveBeenCalledWith( { type: 'organization', id: 'org-1' }, expect.objectContaining({ source: 'reporting' }), @@ -56,7 +55,7 @@ describe('getOrganizationMemberUsageSnapshot', () => { ) }) - it('uses Stripe dates and retains the legacy baseline without custom reporting metadata', async () => { + it('uses Stripe dates without custom reporting metadata', async () => { const periodStart = new Date('2026-08-01T00:00:00.000Z') const periodEnd = new Date('2026-09-01T00:00:00.000Z') getOrganizationSubscription.mockResolvedValue({ @@ -76,6 +75,6 @@ describe('getOrganizationMemberUsageSnapshot', () => { anchorDate: null, interval: 'month', }) - expect(snapshot.includeLegacyBaseline).toBe(true) + expect(snapshot.usageByUser).toEqual(new Map([['user-1', 12.5]])) }) }) diff --git a/apps/sim/lib/billing/core/organization.ts b/apps/sim/lib/billing/core/organization.ts index 1a066efae6f..b3ba3e4e112 100644 --- a/apps/sim/lib/billing/core/organization.ts +++ b/apps/sim/lib/billing/core/organization.ts @@ -10,7 +10,7 @@ import { getBillingPeriodUsageCostByUser, type UsageQueryPeriod, } from '@/lib/billing/core/usage-log' -import { computeOrganizationDailyRefreshConsumed } from '@/lib/billing/credits/daily-refresh' +import { computeDailyRefreshConsumed } from '@/lib/billing/credits/daily-refresh' import { getPlanTierDollars, isEnterprise, isPaid } from '@/lib/billing/plan-helpers' import { getEffectiveSeats, @@ -67,11 +67,10 @@ interface MemberUsageData { } /** - * Per-member usage_log cost for an org's current billing period, keyed by userId. - * `currentPeriodCost` is only a baseline (no longer incremented on the hot path), - * so callers add this ledger component to it for each member's real current-period - * usage. Pass `period` to reuse an already-fetched subscription window; omit it to - * look up the org's subscription here. Returns an empty map when there's no period. + * Per-member usage_log cost for an org's current billing period, keyed by + * userId — each member's real current-period usage. Pass `period` to reuse an + * already-fetched subscription window; omit it to look up the org's + * subscription here. Returns an empty map when there's no period. */ export async function getOrgMemberLedgerByUser( organizationId: string, @@ -96,7 +95,6 @@ export async function getOrgMemberLedgerByUser( export interface OrganizationMemberUsageSnapshot { billingPeriod: UsageQueryPeriod | null - includeLegacyBaseline: boolean usageByUser: Map } @@ -106,13 +104,9 @@ const MAX_ORGANIZATION_BILLING_MEMBER_LIMIT = 100 async function getOrganizationMemberUsageCounts( organizationId: string, billingPeriod: UsageQueryPeriod, - includeLegacyBaseline: boolean, executor: DbClient ): Promise<{ overLimit: number; nearLimit: number }> { - const currentUsage = sql`( - ${includeLegacyBaseline ? sql`coalesce(${userStats.currentPeriodCost}, 0)` : sql`0`} + - coalesce(sum(${usageLog.cost}), 0) - )` + const currentUsage = sql`coalesce(sum(${usageLog.cost}), 0)` .mapWith(Number) .as('current_usage') const usageLimit = sql`coalesce(${userStats.currentUsageLimit}, ${getFreeTierLimit()})` @@ -140,7 +134,7 @@ async function getOrganizationMemberUsageCounts( ) ) .where(eq(member.organizationId, organizationId)) - .groupBy(member.userId, userStats.currentPeriodCost, userStats.currentUsageLimit) + .groupBy(member.userId, userStats.currentUsageLimit) .as('organization_member_usage') const [counts] = await executor @@ -164,8 +158,7 @@ async function getOrganizationMemberUsageCounts( /** * Resolves the organization's usage period once and returns the ledger usage - * for only the requested actors. Reporting periods never include the legacy - * userStats baseline; Stripe/default periods retain it for compatibility. + * for only the requested actors. */ export async function getOrganizationMemberUsageSnapshot( organizationId: string, @@ -179,7 +172,6 @@ export async function getOrganizationMemberUsageSnapshot( const billingPeriod = subscription ? resolveSubscriptionUsagePeriodOrDefault(subscription) : null return { billingPeriod, - includeLegacyBaseline: billingPeriod?.source !== 'reporting', usageByUser: billingPeriod ? await getOrgMemberLedgerByUser(organizationId, billingPeriod, executor, options.userIds) : new Map(), @@ -218,7 +210,6 @@ export async function getOrganizationBillingData( } const billingPeriod = resolveSubscriptionUsagePeriodOrDefault(subscription) - const includeLegacyBaseline = billingPeriod?.source !== 'reporting' const limit = Math.min( MAX_ORGANIZATION_BILLING_MEMBER_LIMIT, Math.max(1, memberPage.limit ?? DEFAULT_ORGANIZATION_BILLING_MEMBER_LIMIT) @@ -226,12 +217,8 @@ export async function getOrganizationBillingData( const offset = Math.max(0, memberPage.offset ?? 0) const [memberAggregateRows, membersWithUsage] = await Promise.all([ executor - .select({ - total: count(), - baseline: sql`coalesce(sum(${userStats.currentPeriodCost}), 0)`, - }) + .select({ total: count() }) .from(member) - .leftJoin(userStats, eq(member.userId, userStats.userId)) .where(eq(member.organizationId, organizationId)), executor .select({ @@ -240,7 +227,6 @@ export async function getOrganizationBillingData( userEmail: user.email, role: member.role, joinedAt: member.createdAt, - currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, }) .from(member) @@ -257,9 +243,7 @@ export async function getOrganizationBillingData( : new Map() const members: MemberUsageData[] = membersWithUsage.map((memberRecord) => { - const currentUsage = - (includeLegacyBaseline ? Number(memberRecord.currentPeriodCost || 0) : 0) + - (usageByUser.get(memberRecord.userId) ?? 0) + const currentUsage = usageByUser.get(memberRecord.userId) ?? 0 const usageLimit = Number(memberRecord.currentUsageLimit || getFreeTierLimit()) const percentUsed = usageLimit > 0 ? (currentUsage / usageLimit) * 100 : 0 @@ -278,22 +262,21 @@ export async function getOrganizationBillingData( const memberAggregate = memberAggregateRows[0] const membersTotal = memberAggregate?.total ?? 0 - let totalCurrentUsage = includeLegacyBaseline ? Number(memberAggregate?.baseline ?? 0) : 0 - if (billingPeriod) { - totalCurrentUsage += await getBillingPeriodUsageCost( - { type: 'organization', id: subscription.referenceId }, - billingPeriod, - undefined, - executor - ) - } + let totalCurrentUsage = billingPeriod + ? await getBillingPeriodUsageCost( + { type: 'organization', id: subscription.referenceId }, + billingPeriod, + undefined, + executor + ) + : 0 if (isPaid(subscription.plan) && subscription.periodStart) { const planDollars = getPlanTierDollars(subscription.plan) if (planDollars > 0) { - const refreshConsumed = await computeOrganizationDailyRefreshConsumed( + const refreshConsumed = await computeDailyRefreshConsumed( { - organizationId: subscription.referenceId, + billingEntity: { type: 'organization', id: subscription.referenceId }, periodStart: subscription.periodStart, periodEnd: subscription.periodEnd ?? null, planDollars, @@ -338,12 +321,7 @@ export async function getOrganizationBillingData( const pendingSeats = await countPendingSeatInvitations(organizationId, executor) const usedSeats = membersTotal + pendingSeats const memberUsageCounts = billingPeriod - ? await getOrganizationMemberUsageCounts( - organizationId, - billingPeriod, - includeLegacyBaseline, - executor - ) + ? await getOrganizationMemberUsageCounts(organizationId, billingPeriod, executor) : { overLimit: 0, nearLimit: 0 } const billingPeriodStart = billingPeriod?.start ?? null diff --git a/apps/sim/lib/billing/core/usage-log.ts b/apps/sim/lib/billing/core/usage-log.ts index 285bb827ca1..552f15b4e2f 100644 --- a/apps/sim/lib/billing/core/usage-log.ts +++ b/apps/sim/lib/billing/core/usage-log.ts @@ -180,8 +180,8 @@ async function resolveBillingContext( } /** - * Returns post-cutover usage for an attributed billing entity/period. - * Legacy pre-cutover usage remains in userStats as a baseline until reset. + * Returns attributed ledger usage for a billing entity/period. The ledger is + * the sole source of truth for usage — there is no userStats baseline. */ export async function getBillingPeriodUsageCost( billingEntity: BillingEntity, @@ -333,6 +333,47 @@ export async function getBillingPeriodUsageCostByUser( return new Map(rows.map((row) => [row.userId, Number.parseFloat(row.cost ?? '0')])) } +/** + * Per-user ledger cost for every stamped billing period fully contained in + * `[from, to]`. Rows are matched on their write-time period stamps + * (`billing_period_start >= from AND billing_period_end <= to`), not on + * `created_at`, so a row written moments after rollover but stamped with the + * prior period is still attributed to that prior period. + * + * Used by the cycle-close sweep, whose window is normally exactly one period + * (`from` = the closed period's start, `to` = its end == the current period's + * start); a wider window absorbs multi-period catch-up after missed sweeps. + */ +export async function getStampedPeriodRangeUsageCostByUser( + billingEntity: BillingEntity, + range: { from: Date; to: Date }, + source?: UsageLogSource | UsageLogSource[], + executor: DbClient = db +): Promise> { + const conditions = [ + eq(usageLog.billingEntityType, billingEntity.type), + eq(usageLog.billingEntityId, billingEntity.id), + gte(usageLog.billingPeriodStart, range.from), + lte(usageLog.billingPeriodEnd, range.to), + ] + if (source) { + conditions.push( + Array.isArray(source) ? inArray(usageLog.source, source) : eq(usageLog.source, source) + ) + } + + const rows = await executor + .select({ + userId: usageLog.userId, + cost: sql`COALESCE(SUM(${usageLog.cost}), 0)`, + }) + .from(usageLog) + .where(and(...conditions)) + .groupBy(usageLog.userId) + + return new Map(rows.map((row) => [row.userId, Number.parseFloat(row.cost ?? '0')])) +} + /** * Records usage as append-only billing events. * diff --git a/apps/sim/lib/billing/core/usage.test.ts b/apps/sim/lib/billing/core/usage.test.ts index 3d98e15e3c5..d0bca9f4c7c 100644 --- a/apps/sim/lib/billing/core/usage.test.ts +++ b/apps/sim/lib/billing/core/usage.test.ts @@ -60,7 +60,6 @@ vi.mock('@/lib/billing/core/usage-log', () => ({ vi.mock('@/lib/billing/credits/daily-refresh', () => ({ computeDailyRefreshConsumed: vi.fn(), - getOrgMemberRefreshBounds: vi.fn(), })) const { diff --git a/apps/sim/lib/billing/core/usage.ts b/apps/sim/lib/billing/core/usage.ts index d0018768697..51e19699c18 100644 --- a/apps/sim/lib/billing/core/usage.ts +++ b/apps/sim/lib/billing/core/usage.ts @@ -22,12 +22,9 @@ import { type ResolvedUsagePeriod, resolveSubscriptionUsagePeriod, } from '@/lib/billing/core/reporting-period' -import { getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' -import { - computeDailyRefreshConsumed, - getOrgMemberRefreshBounds, -} from '@/lib/billing/credits/daily-refresh' -import { getPlanTierDollars, isEnterprise, isFree, isPaid, isPro } from '@/lib/billing/plan-helpers' +import { type BillingEntity, getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' +import { computeDailyRefreshConsumed } from '@/lib/billing/credits/daily-refresh' +import { getPlanTierDollars, isEnterprise, isFree, isPaid } from '@/lib/billing/plan-helpers' import { canEditUsageLimit, getFreeTierLimit, @@ -66,45 +63,29 @@ export interface UsageLimitSubscription { } /** - * Sum `currentPeriodCost` across all members of an organization. - * The single source of truth for pooled-usage reads so every caller - * applies identical null-handling and query shape. Does NOT apply - * daily-refresh deduction — callers layer that on top themselves - * because refresh math needs the caller's `sub` context (plan, - * period, seats, per-user bounds). + * Pooled previous-period bookkeeping total for an organization — the sum of + * member `lastPeriodCost` rows, which the cycle-close sweep writes from + * ledger sums. Current-period usage is never read here — it is always the + * attributed usage_log ledger. * * Uses `LEFT JOIN` so members whose `userStats` row is missing still - * appear (contributing 0), which keeps `memberIds` complete for - * downstream refresh / bounds computations. + * count (contributing 0). */ -export async function getPooledOrgCurrentPeriodCost( +export async function getOrgLastPeriodCost( organizationId: string, executor: DbClient = db -): Promise<{ memberIds: string[]; currentPeriodCost: number; lastPeriodCost: number }> { +): Promise { const rows = await executor - .select({ - userId: member.userId, - currentPeriodCost: userStats.currentPeriodCost, - lastPeriodCost: userStats.lastPeriodCost, - }) + .select({ lastPeriodCost: userStats.lastPeriodCost }) .from(member) .leftJoin(userStats, eq(member.userId, userStats.userId)) .where(eq(member.organizationId, organizationId)) - let pooled = new Decimal(0) let lastPeriodCost = new Decimal(0) - const memberIds: string[] = [] for (const row of rows) { - memberIds.push(row.userId) - pooled = pooled.plus(toDecimal(row.currentPeriodCost)) lastPeriodCost = lastPeriodCost.plus(toDecimal(row.lastPeriodCost)) } - - return { - memberIds, - currentPeriodCost: toNumber(pooled), - lastPeriodCost: toNumber(lastPeriodCost), - } + return toNumber(lastPeriodCost) } /** @@ -242,41 +223,17 @@ export async function getResolvedUserUsageData( interval: null, } - let currentUsageDecimal = toDecimal( - billingPeriod.source === 'reporting' ? 0 : stats.currentPeriodCost - ) - if (!orgScoped) { - currentUsageDecimal = currentUsageDecimal.plus( - await getBillingPeriodUsageCost( + let currentUsage = orgScoped + ? 0 + : await getBillingPeriodUsageCost( { type: 'user', id: userId }, billingPeriod, undefined, executor ) - ) - } - - // For personally-scoped Pro users, include any snapshotted usage from - // a prior org-join so the display reflects their total Pro usage. - if (subscription && isPro(subscription.plan) && !orgScoped) { - const snapshotUsageDecimal = toDecimal(stats.proPeriodCostSnapshot) - if (snapshotUsageDecimal.greaterThan(0)) { - currentUsageDecimal = currentUsageDecimal.plus(snapshotUsageDecimal) - logger.info('Including Pro snapshot in usage display', { - userId, - currentPeriodCost: stats.currentPeriodCost, - proPeriodCostSnapshot: toNumber(snapshotUsageDecimal), - totalUsage: toNumber(currentUsageDecimal), - }) - } - } - let currentUsage = toNumber(currentUsageDecimal) let lastPeriodCost = toNumber(toDecimal(stats.lastPeriodCost)) let limit: number - // Shared between the pooled-usage and pooled-refresh blocks so we - // don't issue the member lookup twice per org-scoped call. - let orgMemberIds: string[] = [] if (orgScoped && subscription) { const orgLimit = await getOrgUsageLimit( @@ -287,17 +244,13 @@ export async function getResolvedUserUsageData( ) limit = orgLimit.limit - const pooled = await getPooledOrgCurrentPeriodCost(subscription.referenceId, executor) - orgMemberIds = pooled.memberIds - lastPeriodCost = pooled.lastPeriodCost - const ledgerUsage = await getBillingPeriodUsageCost( + lastPeriodCost = await getOrgLastPeriodCost(subscription.referenceId, executor) + currentUsage = await getBillingPeriodUsageCost( { type: 'organization', id: subscription.referenceId }, billingPeriod, undefined, executor ) - currentUsage = - (billingPeriod.source === 'reporting' ? 0 : pooled.currentPeriodCost) + ledgerUsage } else { limit = stats.currentUsageLimit ? toNumber(toDecimal(stats.currentUsageLimit)) @@ -311,38 +264,18 @@ export async function getResolvedUserUsageData( if (subscription && isPaid(subscription.plan) && billingPeriodStart) { const planDollars = getPlanTierDollars(subscription.plan) if (planDollars > 0) { - if (orgScoped) { - if (orgMemberIds.length > 0) { - const userBounds = await getOrgMemberRefreshBounds( - subscription.referenceId, - billingPeriodStart, - executor - ) - dailyRefreshConsumed = await computeDailyRefreshConsumed( - { - userIds: orgMemberIds, - periodStart: billingPeriodStart, - periodEnd: billingPeriodEnd, - planDollars, - seats: subscription.seats || 1, - userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined, - billingEntity: { type: 'organization', id: subscription.referenceId }, - }, - executor - ) - } - } else { - dailyRefreshConsumed = await computeDailyRefreshConsumed( - { - userIds: [userId], - periodStart: billingPeriodStart, - periodEnd: billingPeriodEnd, - planDollars, - billingEntity: { type: 'user', id: userId }, - }, - executor - ) - } + dailyRefreshConsumed = await computeDailyRefreshConsumed( + { + billingEntity: orgScoped + ? { type: 'organization', id: subscription.referenceId } + : { type: 'user', id: userId }, + periodStart: billingPeriodStart, + periodEnd: billingPeriodEnd, + planDollars, + seats: orgScoped ? subscription.seats || 1 : undefined, + }, + executor + ) } } @@ -706,51 +639,19 @@ export async function getEffectiveCurrentPeriodCost( const subscription = await getHighestPrioritySubscription(userId, { executor }) const orgScoped = isOrgScopedSubscription(subscription, userId) - let rawCost: number - let refreshUserIds: string[] = [userId] - - if (orgScoped && subscription) { - const pooled = await getPooledOrgCurrentPeriodCost(subscription.referenceId, executor) - if (pooled.memberIds.length === 0) return 0 - refreshUserIds = pooled.memberIds - const billingPeriod = resolveSubscriptionUsagePeriod(subscription) ?? { - ...defaultBillingPeriod(), - source: 'default' as const, - anchorDate: null, - interval: null, - } - rawCost = - (billingPeriod.source === 'reporting' ? 0 : pooled.currentPeriodCost) + - (await getBillingPeriodUsageCost( - { type: 'organization', id: subscription.referenceId }, - billingPeriod, - undefined, - executor - )) - } else { - const rows = await executor - .select({ current: userStats.currentPeriodCost }) - .from(userStats) - .where(eq(userStats.userId, userId)) - .limit(1) - - if (rows.length === 0) return 0 - const billingPeriod = resolveSubscriptionUsagePeriod(subscription) ?? { - ...defaultBillingPeriod(), - source: 'default' as const, - anchorDate: null, - interval: null, - } - rawCost = - (billingPeriod.source === 'reporting' ? 0 : toNumber(toDecimal(rows[0].current))) + - (await getBillingPeriodUsageCost( - { type: 'user', id: userId }, - billingPeriod, - undefined, - executor - )) + const billingPeriod = resolveSubscriptionUsagePeriod(subscription) ?? { + ...defaultBillingPeriod(), + source: 'default' as const, + anchorDate: null, + interval: null, } + const billingEntity: BillingEntity = + orgScoped && subscription + ? { type: 'organization', id: subscription.referenceId } + : { type: 'user', id: userId } + const rawCost = await getBillingPeriodUsageCost(billingEntity, billingPeriod, undefined, executor) + if (!subscription || !isPaid(subscription.plan) || !subscription.periodStart) { return rawCost } @@ -758,27 +659,13 @@ export async function getEffectiveCurrentPeriodCost( const planDollars = getPlanTierDollars(subscription.plan) if (planDollars <= 0) return rawCost - const userBounds = - orgScoped && subscription.periodStart - ? await getOrgMemberRefreshBounds( - subscription.referenceId, - subscription.periodStart, - executor - ) - : {} - const refreshConsumed = await computeDailyRefreshConsumed( { - userIds: refreshUserIds, + billingEntity, periodStart: subscription.periodStart, periodEnd: subscription.periodEnd ?? null, planDollars, seats: subscription.seats || 1, - userBounds: Object.keys(userBounds).length > 0 ? userBounds : undefined, - billingEntity: - orgScoped && subscription - ? { type: 'organization', id: subscription.referenceId } - : { type: 'user', id: userId }, }, executor ) diff --git a/apps/sim/lib/billing/credits/daily-refresh.test.ts b/apps/sim/lib/billing/credits/daily-refresh.test.ts index 3a97f5dd289..86dbb2c0dd8 100644 --- a/apps/sim/lib/billing/credits/daily-refresh.test.ts +++ b/apps/sim/lib/billing/credits/daily-refresh.test.ts @@ -2,7 +2,7 @@ * @vitest-environment node */ import { dbChainMockFns, drizzleOrmMock, schemaMock } from '@sim/testing' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('drizzle-orm', () => { const sqlTag = () => { @@ -23,9 +23,23 @@ vi.mock('@/lib/billing/constants', () => ({ import { computeBillingPeriodUsageWithDailyRefresh, computeDailyRefreshConsumed, - getDailyRefreshDollars, } from '@/lib/billing/credits/daily-refresh' +/** + * Refresh caps windows at `Date.now()`, so the suite pins the clock after + * every fixture period to stay hermetic on any host date. + */ +const FROZEN_NOW = new Date('2026-08-15T00:00:00.000Z') + +beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(FROZEN_NOW) +}) + +afterAll(() => { + vi.useRealTimers() +}) + describe('computeBillingPeriodUsageWithDailyRefresh', () => { const periodStart = new Date('2026-03-01T00:00:00.000Z') const periodEnd = new Date('2026-04-01T00:00:00.000Z') @@ -44,7 +58,6 @@ describe('computeBillingPeriodUsageWithDailyRefresh', () => { computeBillingPeriodUsageWithDailyRefresh({ billingEntity: { type: 'organization', id: 'org-1' }, billingPeriod: { start: periodStart, end: periodEnd }, - userIds: ['user-1'], refreshPeriodStart: periodStart, refreshPeriodEnd: periodEnd, planDollars: 25, @@ -72,7 +85,6 @@ describe('computeBillingPeriodUsageWithDailyRefresh', () => { end: reportingEnd, source: 'reporting', }, - userIds: ['user-1'], refreshPeriodStart: periodStart, refreshPeriodEnd: periodEnd, planDollars: 25, @@ -89,28 +101,6 @@ describe('computeBillingPeriodUsageWithDailyRefresh', () => { reportingEnd ) }) - - it('preserves a bounded user refresh window without narrowing the ledger total', async () => { - const userStart = new Date('2026-03-10T00:00:00.000Z') - const userEnd = new Date('2026-03-20T00:00:00.000Z') - dbChainMockFns.groupBy.mockResolvedValueOnce([ - { ledgerTotal: '30.00', refreshDayTotal: '0.25' }, - ]) - - await computeBillingPeriodUsageWithDailyRefresh({ - billingEntity: { type: 'organization', id: 'org-1' }, - billingPeriod: { start: periodStart, end: periodEnd }, - userIds: ['bounded-user'], - refreshPeriodStart: periodStart, - refreshPeriodEnd: periodEnd, - planDollars: 25, - userBounds: { 'bounded-user': { userStart, userEnd } }, - }) - - expect(drizzleOrmMock.eq).toHaveBeenCalledWith(schemaMock.usageLog.userId, 'bounded-user') - expect(drizzleOrmMock.gte).toHaveBeenCalledWith(schemaMock.usageLog.createdAt, userStart) - expect(drizzleOrmMock.lt).toHaveBeenCalledWith(schemaMock.usageLog.createdAt, userEnd) - }) }) describe('computeDailyRefreshConsumed', () => { @@ -120,7 +110,7 @@ describe('computeDailyRefreshConsumed', () => { it('returns 0 when planDollars is 0', async () => { const result = await computeDailyRefreshConsumed({ - userIds: ['user-1'], + billingEntity: { type: 'user', id: 'user-1' }, periodStart: new Date('2026-03-01'), planDollars: 0, }) @@ -128,24 +118,71 @@ describe('computeDailyRefreshConsumed', () => { expect(dbChainMockFns.groupBy).not.toHaveBeenCalled() }) - it('returns 0 when userIds is empty', async () => { + it('returns 0 when periodEnd is before periodStart', async () => { const result = await computeDailyRefreshConsumed({ - userIds: [], - periodStart: new Date('2026-03-01'), + billingEntity: { type: 'user', id: 'user-1' }, + periodStart: new Date('2026-03-10'), + periodEnd: new Date('2026-03-01'), planDollars: 25, }) expect(result).toBe(0) - expect(dbChainMockFns.groupBy).not.toHaveBeenCalled() }) - it('returns 0 when periodEnd is before periodStart', async () => { + it('scopes rows by the entity and period stamps, never an actor list', async () => { + dbChainMockFns.groupBy.mockResolvedValueOnce([{ dayIndex: 0, dayTotal: '0.10' }]) + const periodStart = new Date('2026-03-01') + + await computeDailyRefreshConsumed({ + billingEntity: { type: 'organization', id: 'org-1' }, + periodStart, + periodEnd: new Date('2026-03-02'), + planDollars: 25, + }) + + expect(drizzleOrmMock.eq).toHaveBeenCalledWith( + schemaMock.usageLog.billingEntityType, + 'organization' + ) + expect(drizzleOrmMock.eq).toHaveBeenCalledWith(schemaMock.usageLog.billingEntityId, 'org-1') + expect(drizzleOrmMock.eq).toHaveBeenCalledWith( + schemaMock.usageLog.billingPeriodStart, + periodStart + ) + expect(drizzleOrmMock.inArray).not.toHaveBeenCalled() + }) + + it('keeps straggler rows stamped to the period but written after its end', async () => { + // A run that started before the rollover inserts rows stamped with the + // elapsed period after it ended; the stamp-based close bills them, so the + // deduction must include them too (clamped into the final day bucket). + dbChainMockFns.groupBy.mockResolvedValueOnce([{ dayIndex: 30, dayTotal: '0.30' }]) + const periodStart = new Date('2026-03-01') + const periodEnd = new Date('2026-04-01') + const result = await computeDailyRefreshConsumed({ - userIds: ['user-1'], - periodStart: new Date('2026-03-10'), - periodEnd: new Date('2026-03-01'), + billingEntity: { type: 'user', id: 'user-1' }, + periodStart, + periodEnd, planDollars: 25, }) - expect(result).toBe(0) + + expect(result).toBe(0.25) + // Membership is stamp-only: no created-at bound may exclude a row the + // stamped ledger total includes. + expect(drizzleOrmMock.lt).not.toHaveBeenCalledWith(schemaMock.usageLog.createdAt, periodEnd) + expect(drizzleOrmMock.gte).not.toHaveBeenCalled() + }) + + it('rejects windows beyond the supported annual bound', async () => { + await expect( + computeDailyRefreshConsumed({ + billingEntity: { type: 'organization', id: 'org-1' }, + periodStart: new Date('2024-01-01'), + periodEnd: new Date('2026-03-01'), + planDollars: 25, + }) + ).rejects.toThrow('annual bound') + expect(dbChainMockFns.groupBy).not.toHaveBeenCalled() }) it('caps each day at the daily refresh allowance', async () => { @@ -156,7 +193,7 @@ describe('computeDailyRefreshConsumed', () => { ]) const result = await computeDailyRefreshConsumed({ - userIds: ['user-1'], + billingEntity: { type: 'user', id: 'user-1' }, periodStart: new Date('2026-03-01'), periodEnd: new Date('2026-03-04'), planDollars: 25, @@ -174,7 +211,7 @@ describe('computeDailyRefreshConsumed', () => { dbChainMockFns.groupBy.mockResolvedValueOnce([]) const result = await computeDailyRefreshConsumed({ - userIds: ['user-1'], + billingEntity: { type: 'user', id: 'user-1' }, periodStart: new Date('2026-03-01'), periodEnd: new Date('2026-03-04'), planDollars: 25, @@ -187,7 +224,7 @@ describe('computeDailyRefreshConsumed', () => { dbChainMockFns.groupBy.mockResolvedValueOnce([{ dayIndex: 0, dayTotal: '2.00' }]) const result = await computeDailyRefreshConsumed({ - userIds: ['user-1', 'user-2', 'user-3'], + billingEntity: { type: 'organization', id: 'org-1' }, periodStart: new Date('2026-03-01'), periodEnd: new Date('2026-03-02'), planDollars: 100, @@ -203,7 +240,7 @@ describe('computeDailyRefreshConsumed', () => { dbChainMockFns.groupBy.mockResolvedValueOnce([{ dayIndex: 0, dayTotal: '50.00' }]) const result = await computeDailyRefreshConsumed({ - userIds: ['user-1', 'user-2'], + billingEntity: { type: 'organization', id: 'org-1' }, periodStart: new Date('2026-03-01'), periodEnd: new Date('2026-03-02'), planDollars: 100, @@ -219,7 +256,7 @@ describe('computeDailyRefreshConsumed', () => { dbChainMockFns.groupBy.mockResolvedValueOnce([{ dayIndex: 0, dayTotal: null }]) const result = await computeDailyRefreshConsumed({ - userIds: ['user-1'], + billingEntity: { type: 'user', id: 'user-1' }, periodStart: new Date('2026-03-01'), periodEnd: new Date('2026-03-02'), planDollars: 25, @@ -228,17 +265,3 @@ describe('computeDailyRefreshConsumed', () => { expect(result).toBe(0) }) }) - -describe('getDailyRefreshDollars', () => { - it('computes correct daily refresh for Pro ($25)', () => { - expect(getDailyRefreshDollars(25)).toBe(0.25) - }) - - it('computes correct daily refresh for Max ($100)', () => { - expect(getDailyRefreshDollars(100)).toBe(1.0) - }) - - it('returns 0 for $0 plan', () => { - expect(getDailyRefreshDollars(0)).toBe(0) - }) -}) diff --git a/apps/sim/lib/billing/credits/daily-refresh.ts b/apps/sim/lib/billing/credits/daily-refresh.ts index 6370556a184..de16c15f63a 100644 --- a/apps/sim/lib/billing/credits/daily-refresh.ts +++ b/apps/sim/lib/billing/credits/daily-refresh.ts @@ -8,13 +8,20 @@ * The total refresh consumed in a period is: * SUM( MIN(day_usage, daily_refresh_amount) ) for each day * - * This is subtracted from `currentPeriodCost` to derive "effective billable usage". + * This is subtracted from ledger period usage to derive "effective billable usage". + * + * Refresh reads are scoped by the ledger's write-time entity and period + * stamps — never by an actor list. Every row attributed to the billing entity + * participates in that entity's refresh, exactly like it participates in the + * entity's ledger total: rows from a member who departed the organization + * mid-period stay stamped to the organization, and a member's pre-join rows + * are user-stamped, so they can never appear under an organization entity. */ import { db } from '@sim/db' -import { member, usageLog, userStats } from '@sim/db/schema' +import { usageLog } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, gte, inArray, isNull, lt, lte, or, sql, sum } from 'drizzle-orm' +import { and, eq, gte, lt, or, sql, sum } from 'drizzle-orm' import { DAILY_REFRESH_RATE } from '@/lib/billing/constants' import type { BillingEntity, UsageQueryPeriod } from '@/lib/billing/core/usage-log' import type { DbClient } from '@/lib/db/types' @@ -24,34 +31,23 @@ const logger = createLogger('DailyRefresh') const MS_PER_DAY = 86_400_000 const MAX_BILLING_PERIOD_DAYS = 370 -/** - * Optional per-user date window. `usageLog` rows outside - * `[userStart, userEnd)` are excluded from that user's contribution. - * Used to slice refresh around a mid-cycle org join so pre-join and - * post-join refresh are billed by the right subscription. - */ -export interface PerUserBounds { - userStart?: Date | null - userEnd?: Date | null -} - interface BillingPeriodUsageWithDailyRefreshParams { billingEntity: BillingEntity billingPeriod: UsageQueryPeriod - userIds: string[] refreshPeriodStart: Date refreshPeriodEnd?: Date | null planDollars: number seats?: number - userBounds?: Record } /** * Reads the exact ledger total and the daily-refresh buckets from one snapshot. * - * The two aggregates intentionally keep different predicates. Ledger totals use - * both captured period bounds (or a reporting-time window), while refresh uses - * the captured period start, eligible users, and per-user time bounds. + * The two aggregates intentionally keep different predicates. Ledger totals + * use both captured period bounds (or a reporting-time window), while refresh + * membership is the captured period-start stamp alone — created-at only + * buckets rows into days, clamped into the period (see + * `computeDailyRefreshConsumed` for why). */ export async function computeBillingPeriodUsageWithDailyRefresh( params: BillingPeriodUsageWithDailyRefreshParams, @@ -60,58 +56,18 @@ export async function computeBillingPeriodUsageWithDailyRefresh( const { billingEntity, billingPeriod, - userIds, refreshPeriodStart, refreshPeriodEnd, planDollars, seats = 1, - userBounds, } = params const now = new Date() const cap = refreshPeriodEnd && refreshPeriodEnd < now ? refreshPeriodEnd : now const dailyRefreshDollars = planDollars * DAILY_REFRESH_RATE * seats - const eligibleUserIds = new Set(userIds) - const unboundedUsers = userBounds ? userIds.filter((id) => !(id in userBounds)) : userIds - const boundedClauses = userBounds - ? Object.entries(userBounds).flatMap(([userId, bounds]) => { - if (!eligibleUserIds.has(userId)) return [] - const effectiveStart = - bounds.userStart && bounds.userStart > refreshPeriodStart - ? bounds.userStart - : refreshPeriodStart - const effectiveEnd = bounds.userEnd && bounds.userEnd < cap ? bounds.userEnd : cap - if (effectiveEnd <= effectiveStart) return [] - return [ - and( - eq(usageLog.userId, userId), - gte(usageLog.createdAt, effectiveStart), - lt(usageLog.createdAt, effectiveEnd) - ), - ] - }) - : [] - const refreshUserFilters = - cap > refreshPeriodStart - ? [ - ...(unboundedUsers.length > 0 - ? [ - and( - inArray(usageLog.userId, unboundedUsers), - gte(usageLog.createdAt, refreshPeriodStart), - lt(usageLog.createdAt, cap) - ), - ] - : []), - ...boundedClauses, - ] - : [] - const refreshFilter = - refreshUserFilters.length > 0 - ? and( - eq(usageLog.billingPeriodStart, refreshPeriodStart), - refreshUserFilters.length === 1 ? refreshUserFilters[0] : or(...refreshUserFilters) - ) - : sql`false` + const refreshWindowActive = cap > refreshPeriodStart + const refreshFilter = refreshWindowActive + ? eq(usageLog.billingPeriodStart, refreshPeriodStart) + : sql`false` const ledgerPeriodFilter = billingPeriod.source === 'reporting' ? and(gte(usageLog.createdAt, billingPeriod.start), lt(usageLog.createdAt, billingPeriod.end)) @@ -123,23 +79,18 @@ export async function computeBillingPeriodUsageWithDailyRefresh( const sameCapturedPeriodStart = billingPeriod.source !== 'reporting' && billingPeriod.start.getTime() === refreshPeriodStart.getTime() - const reportingWindowContainsRefresh = - billingPeriod.source === 'reporting' && - refreshPeriodStart >= billingPeriod.start && - cap <= billingPeriod.end - const scanFilter = - refreshUserFilters.length === 0 - ? ledgerPeriodFilter - : sameCapturedPeriodStart - ? eq(usageLog.billingPeriodStart, billingPeriod.start) - : reportingWindowContainsRefresh - ? ledgerPeriodFilter - : or(ledgerPeriodFilter, refreshFilter) - + const scanFilter = !refreshWindowActive + ? ledgerPeriodFilter + : sameCapturedPeriodStart + ? eq(usageLog.billingPeriodStart, billingPeriod.start) + : or(ledgerPeriodFilter, refreshFilter) + + const startEpoch = Math.floor(refreshPeriodStart.getTime() / 1000) + const capEpoch = Math.floor(cap.getTime() / 1000) const rows = await executor .select({ dayIndex: - sql`FLOOR((EXTRACT(EPOCH FROM ${usageLog.createdAt}) - ${Math.floor(refreshPeriodStart.getTime() / 1000)}) / 86400)`.as( + sql`FLOOR((LEAST(GREATEST(EXTRACT(EPOCH FROM ${usageLog.createdAt}), ${startEpoch}), ${capEpoch - 1}) - ${startEpoch}) / 86400)`.as( 'day_index' ), ledgerTotal: @@ -173,37 +124,30 @@ export async function computeBillingPeriodUsageWithDailyRefresh( } /** - * Compute the total daily refresh credits consumed in the current billing period - * using a single aggregating SQL query grouped by day offset. + * Compute the total daily refresh credits a billing entity consumed in a + * period, using a single aggregating SQL query grouped by day offset. * * For each day from `periodStart`: * consumed_today = MIN(actual_usage_today, daily_refresh_dollars) * + * Rows are scoped purely by the entity and period stamps — see the module + * header for why no actor list participates. + * * @returns Total dollars of refresh consumed across all days (to subtract from usage) */ export async function computeDailyRefreshConsumed( params: { - userIds: string[] + billingEntity: BillingEntity periodStart: Date periodEnd?: Date | null planDollars: number seats?: number - userBounds?: Record - billingEntity?: { type: 'user' | 'organization'; id: string } }, executor: DbClient = db ): Promise { - const { - userIds, - periodStart, - periodEnd, - planDollars, - seats = 1, - userBounds, - billingEntity, - } = params + const { billingEntity, periodStart, periodEnd, planDollars, seats = 1 } = params - if (planDollars <= 0 || userIds.length === 0) return 0 + if (planDollars <= 0) return 0 const dailyRefreshDollars = planDollars * DAILY_REFRESH_RATE * seats @@ -213,60 +157,33 @@ export async function computeDailyRefreshConsumed( if (cap <= periodStart) return 0 const dayCount = Math.ceil((cap.getTime() - periodStart.getTime()) / MS_PER_DAY) - if (dayCount <= 0) return 0 - - const unboundedUsers = userBounds ? userIds.filter((id) => !(id in userBounds)) : userIds - const billingEntityFilter = billingEntity - ? and( - eq(usageLog.billingEntityType, billingEntity.type), - eq(usageLog.billingEntityId, billingEntity.id), - eq(usageLog.billingPeriodStart, periodStart) - ) - : undefined - - const boundedClauses = userBounds - ? Object.entries(userBounds).flatMap(([userId, bounds]) => { - if (!userIds.includes(userId)) return [] - const effectiveStart = - bounds.userStart && bounds.userStart > periodStart ? bounds.userStart : periodStart - const effectiveEnd = bounds.userEnd && bounds.userEnd < cap ? bounds.userEnd : cap - if (effectiveEnd <= effectiveStart) return [] - return [ - and( - eq(usageLog.userId, userId), - billingEntityFilter, - gte(usageLog.createdAt, effectiveStart), - lt(usageLog.createdAt, effectiveEnd) - ), - ] - }) - : [] - - const rowFilters = - unboundedUsers.length > 0 - ? [ - and( - inArray(usageLog.userId, unboundedUsers), - billingEntityFilter, - gte(usageLog.createdAt, periodStart), - lt(usageLog.createdAt, cap) - ), - ...boundedClauses, - ] - : boundedClauses - - if (rowFilters.length === 0) return 0 + if (dayCount > MAX_BILLING_PERIOD_DAYS) { + throw new Error('Billing period exceeds the supported annual bound') + } + // Membership mirrors the ledger sums exactly: the entity and period stamps + // alone. Created-at only assigns the day bucket, clamped into the period — + // a straggler row written after the rollover (billing attribution is frozen + // at run start) is billed by the stamp-based close, so it must consume + // refresh on the period's final day rather than fall out of the deduction. + const startEpoch = Math.floor(periodStart.getTime() / 1000) + const capEpoch = Math.floor(cap.getTime() / 1000) const rows = await executor .select({ dayIndex: - sql`FLOOR((EXTRACT(EPOCH FROM ${usageLog.createdAt}) - ${Math.floor(periodStart.getTime() / 1000)}) / 86400)`.as( + sql`FLOOR((LEAST(GREATEST(EXTRACT(EPOCH FROM ${usageLog.createdAt}), ${startEpoch}), ${capEpoch - 1}) - ${startEpoch}) / 86400)`.as( 'day_index' ), dayTotal: sum(usageLog.cost).as('day_total'), }) .from(usageLog) - .where(rowFilters.length === 1 ? rowFilters[0] : or(...rowFilters)) + .where( + and( + eq(usageLog.billingEntityType, billingEntity.type), + eq(usageLog.billingEntityId, billingEntity.id), + eq(usageLog.billingPeriodStart, periodStart) + ) + ) .groupBy(sql`day_index`) let totalConsumed = 0 @@ -276,101 +193,12 @@ export async function computeDailyRefreshConsumed( } logger.debug('Daily refresh computed', { - userCount: userIds.length, + billingEntityType: billingEntity.type, periodStart: periodStart.toISOString(), days: dayCount, dailyRefreshDollars, totalConsumed, - hasUserBounds: Boolean(userBounds), }) return totalConsumed } - -export async function computeOrganizationDailyRefreshConsumed( - params: { - organizationId: string - periodStart: Date - periodEnd?: Date | null - planDollars: number - seats?: number - }, - executor: DbClient = db -): Promise { - const { organizationId, periodStart, periodEnd, planDollars, seats = 1 } = params - if (planDollars <= 0) return 0 - - const now = new Date() - const cap = periodEnd && periodEnd < now ? periodEnd : now - if (cap <= periodStart) return 0 - const dayCount = Math.ceil((cap.getTime() - periodStart.getTime()) / MS_PER_DAY) - if (dayCount > MAX_BILLING_PERIOD_DAYS) { - throw new Error('Organization billing period exceeds the supported annual bound') - } - - const dailyRefreshDollars = planDollars * DAILY_REFRESH_RATE * seats - const rows = await executor - .select({ - dayIndex: - sql`FLOOR((EXTRACT(EPOCH FROM ${usageLog.createdAt}) - ${Math.floor(periodStart.getTime() / 1000)}) / 86400)`.as( - 'day_index' - ), - dayTotal: sum(usageLog.cost).as('day_total'), - }) - .from(usageLog) - .innerJoin( - member, - and(eq(member.userId, usageLog.userId), eq(member.organizationId, organizationId)) - ) - .leftJoin(userStats, eq(userStats.userId, member.userId)) - .where( - and( - eq(usageLog.billingEntityType, 'organization'), - eq(usageLog.billingEntityId, organizationId), - eq(usageLog.billingPeriodStart, periodStart), - gte(usageLog.createdAt, periodStart), - lt(usageLog.createdAt, cap), - or( - isNull(userStats.proPeriodCostSnapshotAt), - lte(userStats.proPeriodCostSnapshotAt, periodStart), - gte(usageLog.createdAt, userStats.proPeriodCostSnapshotAt) - ) - ) - ) - .groupBy(sql`day_index`) - - return rows.reduce((total, row) => { - const dayUsage = Number.parseFloat(row.dayTotal ?? '0') - return total + Math.min(dayUsage, dailyRefreshDollars) - }, 0) -} - -/** - * Get the daily refresh allowance in dollars for a plan. - */ -export function getDailyRefreshDollars(planDollars: number): number { - return planDollars * DAILY_REFRESH_RATE -} - -export async function getOrgMemberRefreshBounds( - organizationId: string, - periodStart: Date, - executor: DbClient = db -): Promise> { - const rows = await executor - .select({ - userId: member.userId, - snapshotAt: userStats.proPeriodCostSnapshotAt, - }) - .from(member) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, organizationId)) - - const bounds: Record = {} - for (const row of rows) { - if (row.snapshotAt && row.snapshotAt > periodStart) { - bounds[row.userId] = { userStart: row.snapshotAt } - } - } - return bounds -} diff --git a/apps/sim/lib/billing/cycle-close.test.ts b/apps/sim/lib/billing/cycle-close.test.ts new file mode 100644 index 00000000000..5a29cf9fdc2 --- /dev/null +++ b/apps/sim/lib/billing/cycle-close.test.ts @@ -0,0 +1,665 @@ +/** + * @vitest-environment node + */ +import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockComputeOrgOverageAmount, + mockIsSubscriptionOrgScoped, + mockGetStampedPeriodRangeUsageCostByUser, + mockComputeDailyRefreshConsumed, + mockEnqueueOutboxEvent, + mockGetPlanPricing, + mockGetPlanTierDollars, + mockResolveSubscriptionUsagePeriod, + mockIsEnterprise, + mockIsFree, + mockRecordAudit, + mockCaptureServerEvent, +} = vi.hoisted(() => ({ + mockComputeOrgOverageAmount: vi.fn(), + mockIsSubscriptionOrgScoped: vi.fn(), + mockGetStampedPeriodRangeUsageCostByUser: vi.fn(), + mockComputeDailyRefreshConsumed: vi.fn(), + mockEnqueueOutboxEvent: vi.fn(), + mockGetPlanPricing: vi.fn(), + mockGetPlanTierDollars: vi.fn(), + mockResolveSubscriptionUsagePeriod: vi.fn(), + mockIsEnterprise: vi.fn(), + mockIsFree: vi.fn(), + mockRecordAudit: vi.fn(), + mockCaptureServerEvent: vi.fn(), +})) + +vi.mock('@sim/audit', () => ({ + AuditAction: { OVERAGE_BILLED: 'overage.billed' }, + AuditResourceType: { BILLING: 'billing' }, + recordAudit: mockRecordAudit, +})) + +vi.mock('@/lib/billing/core/billing', () => ({ + computeOrgOverageAmount: mockComputeOrgOverageAmount, + isSubscriptionOrgScoped: mockIsSubscriptionOrgScoped, +})) + +vi.mock('@/lib/billing/core/reporting-period', () => ({ + resolveSubscriptionUsagePeriod: mockResolveSubscriptionUsagePeriod, +})) + +vi.mock('@/lib/billing/core/usage-log', () => ({ + COPILOT_USAGE_SOURCES: ['copilot'], + getStampedPeriodRangeUsageCostByUser: mockGetStampedPeriodRangeUsageCostByUser, +})) + +vi.mock('@/lib/billing/credits/daily-refresh', () => ({ + computeDailyRefreshConsumed: mockComputeDailyRefreshConsumed, +})) + +vi.mock('@/lib/billing/plan-helpers', () => ({ + getPlanTierDollars: mockGetPlanTierDollars, + isEnterprise: mockIsEnterprise, + isFree: mockIsFree, +})) + +vi.mock('@/lib/billing/subscriptions/utils', () => ({ + ENTITLED_SUBSCRIPTION_STATUSES: ['active', 'past_due'], + getPlanPricing: mockGetPlanPricing, +})) + +vi.mock('@/lib/billing/webhooks/outbox-handlers', () => ({ + OUTBOX_EVENT_TYPES: { + STRIPE_THRESHOLD_OVERAGE_INVOICE: 'stripe.threshold-overage-invoice', + }, +})) + +vi.mock('@/lib/core/outbox/service', () => ({ + enqueueOutboxEvent: mockEnqueueOutboxEvent, +})) + +vi.mock('@/lib/posthog/server', () => ({ + captureServerEvent: mockCaptureServerEvent, +})) + +import { + claimTerminalPeriod, + closeElapsedBillingPeriod, + closeElapsedPeriodBeforeDeletion, + isSubscriptionCycleCloseCurrent, + sweepBillingCycleCloses, + writeFinalPeriodBookkeeping, +} from '@/lib/billing/cycle-close' + +type SubInput = Parameters[0] + +const PERIOD_START = new Date('2026-08-01T00:00:00.000Z') +const PREV_PERIOD_START = new Date('2026-07-01T00:00:00.000Z') + +/** + * The grace gate and lagging checks compare fixed period boundaries against + * `Date.now()`, so the suite pins the clock to stay hermetic on any host date. + */ +const FROZEN_NOW = new Date('2026-08-15T00:00:00.000Z') + +beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(FROZEN_NOW) +}) + +afterAll(() => { + vi.useRealTimers() +}) + +function subRow(overrides: Partial> = {}): SubInput { + return { + id: 'sub-1', + plan: 'team', + referenceId: 'org-1', + stripeCustomerId: 'cus_1', + stripeSubscriptionId: 'sub_stripe_1', + status: 'active', + periodStart: PERIOD_START, + periodEnd: new Date('2026-09-01T00:00:00.000Z'), + billingInterval: 'month', + metadata: null, + lastClosedPeriodStart: PREV_PERIOD_START, + ...overrides, + } as SubInput +} + +/** + * Queues the org close's reads in table order: member roster, in-tx member + * userStats lock, organization credit row, subscription marker re-read, the + * under-lock roster revalidation, and the tracker userStats row. + */ +function queueOrgCloseReads({ + members = [{ userId: 'owner-1', role: 'owner' }], + orgRow = { creditBalance: '0' }, + markerRow = { lastClosedPeriodStart: PREV_PERIOD_START }, + lockedRoster = members, + trackerRow = { billedOverageThisPeriod: '0', creditBalance: '0' }, +}: { + members?: { userId: string; role: string }[] + orgRow?: Record + markerRow?: Record + lockedRoster?: { userId: string; role: string }[] + trackerRow?: Record +} = {}) { + queueTableRows(schemaMock.member, members) + queueTableRows(schemaMock.userStats, []) + queueTableRows(schemaMock.organization, [orgRow]) + queueTableRows(schemaMock.subscription, [markerRow]) + queueTableRows(schemaMock.member, lockedRoster) + queueTableRows(schemaMock.userStats, [trackerRow]) +} + +describe('closeElapsedBillingPeriod', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mockIsSubscriptionOrgScoped.mockResolvedValue(true) + mockIsEnterprise.mockReturnValue(false) + mockIsFree.mockReturnValue(false) + mockResolveSubscriptionUsagePeriod.mockReturnValue(null) + mockGetPlanTierDollars.mockReturnValue(40) + mockGetPlanPricing.mockReturnValue({ basePrice: 40 }) + mockComputeDailyRefreshConsumed.mockResolvedValue(0) + mockGetStampedPeriodRangeUsageCostByUser.mockResolvedValue(new Map([['owner-1', 150]])) + mockComputeOrgOverageAmount.mockResolvedValue({ + effectiveUsage: 150, + baseSubscriptionAmount: 80, + dailyRefreshDeduction: 0, + totalOverage: 70, + }) + dbChainMockFns.returning.mockResolvedValue([{ id: 'sub-1' }]) + }) + + afterAll(() => { + resetDbChainMock() + }) + + it('initializes a null marker without billing', async () => { + const result = await closeElapsedBillingPeriod(subRow({ lastClosedPeriodStart: null })) + + expect(result.status).toBe('initialized') + expect(dbChainMockFns.update).toHaveBeenCalledTimes(1) + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + expect(mockGetStampedPeriodRangeUsageCostByUser).not.toHaveBeenCalled() + }) + + it('returns current when the marker already matches the period start', async () => { + const result = await closeElapsedBillingPeriod(subRow({ lastClosedPeriodStart: PERIOD_START })) + + expect(result.status).toBe('current') + expect(dbChainMockFns.update).not.toHaveBeenCalled() + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + }) + + it('closes a team period: bills the remainder, resets trackers, and claims the marker', async () => { + queueOrgCloseReads() + + const result = await closeElapsedBillingPeriod(subRow()) + + expect(result.status).toBe('closed') + expect(result.overageBilled).toBe(70) + + expect(mockComputeOrgOverageAmount).toHaveBeenCalledWith({ + plan: 'team', + seats: null, + periodStart: PREV_PERIOD_START, + periodEnd: PERIOD_START, + organizationId: 'org-1', + pooledLedgerUsage: 150, + }) + + expect(mockEnqueueOutboxEvent).toHaveBeenCalledTimes(1) + const [, eventType, payload] = mockEnqueueOutboxEvent.mock.calls[0] + expect(eventType).toBe('stripe.threshold-overage-invoice') + expect(payload).toMatchObject({ + customerId: 'cus_1', + stripeSubscriptionId: 'sub_stripe_1', + amountCents: 7000, + invoiceIdemKeyStem: `cycle-close-overage:sub-1:${PERIOD_START.toISOString()}:invoice`, + metadata: expect.objectContaining({ type: 'overage_billing', organizationId: 'org-1' }), + }) + + // Bookkeeping: last-period CASE write + billedOverage reset on member rows. + const bookkeepingSet = dbChainMockFns.set.mock.calls.find( + (call) => (call[0] as Record).billedOverageThisPeriod === '0' + )?.[0] as Record + expect(bookkeepingSet).toBeDefined() + expect( + (bookkeepingSet.lastPeriodCost as { toSQL?: () => { sql: string } })?.toSQL?.().sql + ).toContain('CASE') + + // Marker claim committed in the same transaction. + const markerSet = dbChainMockFns.set.mock.calls.find( + (call) => (call[0] as Record).lastClosedPeriodStart instanceof Date + ) + expect(markerSet).toBeDefined() + expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(1) + expect(mockRecordAudit).toHaveBeenCalledTimes(1) + expect(mockCaptureServerEvent).toHaveBeenCalledTimes(1) + }) + + it('defers the close inside the settlement grace after a rollover', async () => { + // A run whose frozen attribution predates the rollover could still insert + // elapsed-period rows; the close waits until sums are final. + const result = await closeElapsedBillingPeriod( + subRow({ + periodStart: new Date(Date.now() - 60_000), + lastClosedPeriodStart: new Date(Date.now() - 60_000 - 31 * 24 * 60 * 60 * 1000), + }) + ) + + expect(result.status).toBe('skipped') + expect(dbChainMockFns.transaction).not.toHaveBeenCalled() + expect(mockGetStampedPeriodRangeUsageCostByUser).not.toHaveBeenCalled() + }) + + it('defers the close when overage is due but Stripe identifiers are missing', async () => { + const result = await closeElapsedBillingPeriod(subRow({ stripeCustomerId: null })) + + expect(result.status).toBe('skipped') + // No marker claim, no money, no bookkeeping — the sweep retries next run. + expect(dbChainMockFns.transaction).not.toHaveBeenCalled() + expect(dbChainMockFns.update).not.toHaveBeenCalled() + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + }) + + it('derives the closed window from the ledger period stamps when they drift from calendar math', async () => { + // Rows for the elapsed period are stamped starting Jul 3 (anchor drift) + // while the marker sits at Jul 1: only the stamp lookup can produce the + // Jul 3 bound — calendar math (periodStart minus one interval) would keep + // the window at Jul 1. + const stampedPrevStart = new Date('2026-07-03T00:00:00.000Z') + queueTableRows(schemaMock.usageLog, [{ start: stampedPrevStart }]) + queueOrgCloseReads() + + await closeElapsedBillingPeriod(subRow({ lastClosedPeriodStart: PREV_PERIOD_START })) + + expect(mockComputeOrgOverageAmount).toHaveBeenCalledWith( + expect.objectContaining({ periodStart: stampedPrevStart, periodEnd: PERIOD_START }) + ) + }) + + it('defers the close when overage is due but the organization has no owner', async () => { + mockGetStampedPeriodRangeUsageCostByUser.mockResolvedValue(new Map([['departed-1', 150]])) + // Member roster has no owner-role row. + queueTableRows(schemaMock.member, [{ userId: 'member-1', role: 'member' }]) + + const result = await closeElapsedBillingPeriod(subRow()) + + expect(result.status).toBe('skipped') + expect(dbChainMockFns.transaction).not.toHaveBeenCalled() + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + }) + + it('bills departed members through the pooled entity sums, not a roster', async () => { + // 'departed-1' has org-attributed rows in the closed period but no member + // row anymore; the pooled sum carries them, and the entity-scoped refresh + // inside computeOrgOverageAmount offsets them identically — no actor list + // is passed anywhere. + mockGetStampedPeriodRangeUsageCostByUser.mockResolvedValue( + new Map([ + ['owner-1', 100], + ['departed-1', 50], + ]) + ) + queueOrgCloseReads() + + await closeElapsedBillingPeriod(subRow()) + + expect(mockComputeOrgOverageAmount).toHaveBeenCalledWith({ + plan: 'team', + seats: null, + periodStart: PREV_PERIOD_START, + periodEnd: PERIOD_START, + organizationId: 'org-1', + pooledLedgerUsage: 150, + }) + }) + + it('subtracts the current period tracker from the final overage', async () => { + queueOrgCloseReads({ trackerRow: { billedOverageThisPeriod: '30', creditBalance: '0' } }) + + const result = await closeElapsedBillingPeriod(subRow()) + + expect(result.status).toBe('closed') + expect(result.overageBilled).toBe(40) + const [, , payload] = mockEnqueueOutboxEvent.mock.calls[0] + expect(payload).toMatchObject({ amountCents: 4000 }) + }) + + it('ignores the stale tracker when the close skipped forgiven periods', async () => { + // Marker two intervals back: the close forgives the older period and + // bills [Jul 1, Aug 1) only. The tracker's collections belong to the + // period that began at the marker, so none of them offset this close. + const staleMarker = new Date('2026-05-01T00:00:00.000Z') + queueOrgCloseReads({ + markerRow: { lastClosedPeriodStart: staleMarker }, + trackerRow: { billedOverageThisPeriod: '30', creditBalance: '0' }, + }) + + const result = await closeElapsedBillingPeriod(subRow({ lastClosedPeriodStart: staleMarker })) + + expect(result.status).toBe('closed') + expect(result.overageBilled).toBe(70) + const [, , payload] = mockEnqueueOutboxEvent.mock.calls[0] + expect(payload).toMatchObject({ amountCents: 7000 }) + }) + + it('applies organization credits before invoicing and skips Stripe when covered', async () => { + queueOrgCloseReads({ orgRow: { creditBalance: '100' } }) + + const result = await closeElapsedBillingPeriod(subRow()) + + expect(result.status).toBe('closed') + expect(result.creditsApplied).toBe(70) + expect(result.overageBilled).toBe(0) + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + expect(mockRecordAudit).toHaveBeenCalledTimes(1) + }) + + it('subtracts overage already collected by threshold billing', async () => { + queueOrgCloseReads({ trackerRow: { billedOverageThisPeriod: '70', creditBalance: '0' } }) + + const result = await closeElapsedBillingPeriod(subRow()) + + expect(result.status).toBe('closed') + expect(result.overageBilled).toBe(0) + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + expect(mockRecordAudit).not.toHaveBeenCalled() + }) + + it('defers when the organization roster changed between preflight and the locked transaction', async () => { + queueOrgCloseReads({ + lockedRoster: [ + { userId: 'owner-1', role: 'member' }, + { userId: 'member-2', role: 'owner' }, + ], + }) + + const result = await closeElapsedBillingPeriod(subRow()) + + expect(result.status).toBe('skipped') + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + expect(dbChainMockFns.update).not.toHaveBeenCalled() + }) + + it('no-ops when a concurrent closer already advanced the marker', async () => { + queueOrgCloseReads({ markerRow: { lastClosedPeriodStart: PERIOD_START } }) + + const result = await closeElapsedBillingPeriod(subRow()) + + expect(result.status).toBe('already-closed') + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + expect(dbChainMockFns.update).not.toHaveBeenCalled() + }) + + it('books enterprise periods without collecting money', async () => { + mockIsEnterprise.mockReturnValue(true) + queueOrgCloseReads() + + const result = await closeElapsedBillingPeriod(subRow({ plan: 'enterprise' })) + + expect(result.status).toBe('closed') + expect(result.overageBilled).toBe(0) + expect(mockComputeOrgOverageAmount).not.toHaveBeenCalled() + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + // Bookkeeping still writes last-period sums. + const bookkeepingSet = dbChainMockFns.set.mock.calls.find( + (call) => (call[0] as Record).billedOverageThisPeriod === '0' + ) + expect(bookkeepingSet).toBeDefined() + }) + + it('only advances the marker for enterprise orgs on reporting anchors', async () => { + mockIsEnterprise.mockReturnValue(true) + mockResolveSubscriptionUsagePeriod.mockReturnValue({ source: 'reporting' }) + + const result = await closeElapsedBillingPeriod( + subRow({ plan: 'enterprise', metadata: { reportingPeriodAnchorDate: '2026-05-01' } }) + ) + + expect(result.status).toBe('closed') + expect(mockGetStampedPeriodRangeUsageCostByUser).not.toHaveBeenCalled() + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + }) + + it('closes a personal subscription against the user ledger', async () => { + mockIsSubscriptionOrgScoped.mockResolvedValue(false) + mockGetStampedPeriodRangeUsageCostByUser.mockResolvedValue(new Map([['user-1', 90]])) + // Personal reads: in-tx userStats lock, marker re-read, tracker row. + queueTableRows(schemaMock.userStats, []) + queueTableRows(schemaMock.subscription, [{ lastClosedPeriodStart: PREV_PERIOD_START }]) + queueTableRows(schemaMock.userStats, [{ billedOverageThisPeriod: '0', creditBalance: '0' }]) + + const result = await closeElapsedBillingPeriod( + subRow({ plan: 'pro', referenceId: 'user-1', stripeCustomerId: 'cus_user' }) + ) + + expect(result.status).toBe('closed') + // 90 ledger - 0 refresh - 40 base = 50 overage + expect(result.overageBilled).toBe(50) + expect(mockComputeOrgOverageAmount).not.toHaveBeenCalled() + expect(mockEnqueueOutboxEvent).toHaveBeenCalledTimes(1) + }) +}) + +describe('writeFinalPeriodBookkeeping', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mockIsSubscriptionOrgScoped.mockResolvedValue(true) + mockIsEnterprise.mockReturnValue(false) + mockResolveSubscriptionUsagePeriod.mockReturnValue(null) + mockGetStampedPeriodRangeUsageCostByUser.mockResolvedValue(new Map([['owner-1', 25]])) + dbChainMockFns.returning.mockResolvedValue([{ id: 'sub-1' }]) + }) + + it('resets trackers and writes last-period sums in one transaction', async () => { + queueTableRows(schemaMock.member, [{ userId: 'owner-1' }]) + + await writeFinalPeriodBookkeeping({ + id: 'sub-1', + plan: 'team', + referenceId: 'org-1', + periodStart: PERIOD_START, + periodEnd: new Date('2026-09-01T00:00:00.000Z'), + }) + + expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(1) + const bookkeepingSet = dbChainMockFns.set.mock.calls.find( + (call) => (call[0] as Record).billedOverageThisPeriod === '0' + ) + expect(bookkeepingSet).toBeDefined() + }) + + it('is a no-op for reporting-anchor enterprise subscriptions', async () => { + mockIsEnterprise.mockReturnValue(true) + mockResolveSubscriptionUsagePeriod.mockReturnValue({ source: 'reporting' }) + + await writeFinalPeriodBookkeeping({ + id: 'sub-1', + plan: 'enterprise', + referenceId: 'org-1', + periodStart: PERIOD_START, + periodEnd: new Date('2026-09-01T00:00:00.000Z'), + metadata: { reportingPeriodAnchorDate: '2026-05-01' }, + }) + + expect(mockGetStampedPeriodRangeUsageCostByUser).not.toHaveBeenCalled() + expect(dbChainMockFns.transaction).not.toHaveBeenCalled() + expect(dbChainMockFns.set).not.toHaveBeenCalled() + }) +}) + +describe('closeElapsedPeriodBeforeDeletion', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mockIsSubscriptionOrgScoped.mockResolvedValue(true) + mockIsEnterprise.mockReturnValue(false) + mockIsFree.mockReturnValue(false) + mockResolveSubscriptionUsagePeriod.mockReturnValue(null) + mockGetPlanTierDollars.mockReturnValue(40) + mockGetPlanPricing.mockReturnValue({ basePrice: 40 }) + mockComputeDailyRefreshConsumed.mockResolvedValue(0) + mockGetStampedPeriodRangeUsageCostByUser.mockResolvedValue(new Map([['owner-1', 150]])) + mockComputeOrgOverageAmount.mockResolvedValue({ + effectiveUsage: 150, + baseSubscriptionAmount: 80, + dailyRefreshDeduction: 0, + totalOverage: 70, + }) + dbChainMockFns.returning.mockResolvedValue([{ id: 'sub-1' }]) + }) + + it('closes a lagging period with the settlement grace bypassed', async () => { + // Rollover 30 minutes ago — inside the grace the sweep would honor. The + // deletion path cannot wait: no later sweep revisits a canceled sub, so + // it settles the elapsed period now with whatever rows have landed. + const recentRollover = new Date(Date.now() - 30 * 60 * 1000) + queueTableRows(schemaMock.subscription, [subRow({ periodStart: recentRollover })]) + queueOrgCloseReads() + + await closeElapsedPeriodBeforeDeletion('sub-1') + + expect(mockEnqueueOutboxEvent).toHaveBeenCalledTimes(1) + const markerSet = dbChainMockFns.set.mock.calls.find( + (call) => (call[0] as Record).lastClosedPeriodStart instanceof Date + ) + expect(markerSet).toBeDefined() + }) + + it('no-ops when the close marker is already current', async () => { + queueTableRows(schemaMock.subscription, [subRow({ lastClosedPeriodStart: PERIOD_START })]) + + await closeElapsedPeriodBeforeDeletion('sub-1') + + expect(dbChainMockFns.transaction).not.toHaveBeenCalled() + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + }) +}) + +describe('claimTerminalPeriod', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + dbChainMockFns.returning.mockResolvedValue([{ id: 'sub-1' }]) + }) + + it('returns the fresh period without rewriting a current marker', async () => { + queueTableRows(schemaMock.subscription, [ + { + periodStart: PERIOD_START, + periodEnd: new Date('2026-09-01T00:00:00.000Z'), + lastClosedPeriodStart: PERIOD_START, + }, + ]) + + const terminal = await claimTerminalPeriod('sub-1') + + expect(terminal.periodStart).toEqual(PERIOD_START) + expect(terminal.markerWasCurrent).toBe(true) + expect(dbChainMockFns.set).not.toHaveBeenCalled() + }) + + it('reports a lagging marker without jumping it, so the caller can close and re-claim', async () => { + queueTableRows(schemaMock.subscription, [ + { + periodStart: PERIOD_START, + periodEnd: new Date('2026-09-01T00:00:00.000Z'), + lastClosedPeriodStart: PREV_PERIOD_START, + }, + ]) + + const terminal = await claimTerminalPeriod('sub-1') + + expect(terminal.markerWasCurrent).toBe(false) + expect(dbChainMockFns.set).not.toHaveBeenCalled() + }) + + it('seals a lagging marker on request, forgiving the unclosed period loudly', async () => { + queueTableRows(schemaMock.subscription, [ + { + periodStart: PERIOD_START, + periodEnd: new Date('2026-09-01T00:00:00.000Z'), + lastClosedPeriodStart: PREV_PERIOD_START, + }, + ]) + + const terminal = await claimTerminalPeriod('sub-1', { sealLagging: true }) + + expect(terminal.markerWasCurrent).toBe(false) + const markerSet = dbChainMockFns.set.mock.calls.find( + (call) => (call[0] as Record).lastClosedPeriodStart instanceof Date + ) + expect(markerSet).toBeDefined() + }) + + it('returns nulls without claiming when the subscription has no period', async () => { + queueTableRows(schemaMock.subscription, [{ periodStart: null, periodEnd: null }]) + + const terminal = await claimTerminalPeriod('sub-1') + + expect(terminal).toEqual({ periodStart: null, periodEnd: null, markerWasCurrent: true }) + expect(dbChainMockFns.set).not.toHaveBeenCalled() + }) +}) + +describe('isSubscriptionCycleCloseCurrent', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('is current when the marker has caught up to the period start', async () => { + queueTableRows(schemaMock.subscription, [ + { periodStart: PERIOD_START, lastClosedPeriodStart: PERIOD_START }, + ]) + await expect(isSubscriptionCycleCloseCurrent('sub-1')).resolves.toBe(true) + }) + + it('is pending when the marker lags the period start or was never initialized', async () => { + queueTableRows(schemaMock.subscription, [ + { periodStart: PERIOD_START, lastClosedPeriodStart: PREV_PERIOD_START }, + ]) + await expect(isSubscriptionCycleCloseCurrent('sub-1')).resolves.toBe(false) + + queueTableRows(schemaMock.subscription, [ + { periodStart: PERIOD_START, lastClosedPeriodStart: null }, + ]) + await expect(isSubscriptionCycleCloseCurrent('sub-1')).resolves.toBe(false) + }) + + it('is current when the subscription has no period to close', async () => { + queueTableRows(schemaMock.subscription, [{ periodStart: null, lastClosedPeriodStart: null }]) + await expect(isSubscriptionCycleCloseCurrent('sub-1')).resolves.toBe(true) + }) +}) + +describe('sweepBillingCycleCloses', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mockIsFree.mockReturnValue(false) + mockIsEnterprise.mockReturnValue(false) + }) + + it('initializes every candidate with a lagging marker', async () => { + // Both rows are shaped like rows the sweep's candidate query can actually + // return: entitled, non-null periodStart, marker lagging (null). + queueTableRows(schemaMock.subscription, [ + subRow({ id: 'sub-a', lastClosedPeriodStart: null }), + subRow({ id: 'sub-b', lastClosedPeriodStart: null }), + ]) + + const summary = await sweepBillingCycleCloses() + + expect(summary.candidates).toBe(2) + expect(summary.initialized).toBe(2) + expect(summary.failed).toBe(0) + }) +}) diff --git a/apps/sim/lib/billing/cycle-close.ts b/apps/sim/lib/billing/cycle-close.ts new file mode 100644 index 00000000000..3573bb95824 --- /dev/null +++ b/apps/sim/lib/billing/cycle-close.ts @@ -0,0 +1,852 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { db } from '@sim/db' +import { + member, + organization, + subscription as subscriptionTable, + usageLog, + userStats, +} from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { and, eq, inArray, isNull, lt, or, sql } from 'drizzle-orm' +import { BILLING_LOCK_TIMEOUT_MS } from '@/lib/billing/constants' +import { computeOrgOverageAmount, isSubscriptionOrgScoped } from '@/lib/billing/core/billing' +import { resolveSubscriptionUsagePeriod } from '@/lib/billing/core/reporting-period' +import { + COPILOT_USAGE_SOURCES, + getStampedPeriodRangeUsageCostByUser, +} from '@/lib/billing/core/usage-log' +import { computeDailyRefreshConsumed } from '@/lib/billing/credits/daily-refresh' +import { getPlanTierDollars, isEnterprise, isFree } from '@/lib/billing/plan-helpers' +import { ENTITLED_SUBSCRIPTION_STATUSES, getPlanPricing } from '@/lib/billing/subscriptions/utils' +import { toDecimal, toNumber } from '@/lib/billing/utils/decimal' +import { OUTBOX_EVENT_TYPES } from '@/lib/billing/webhooks/outbox-handlers' +import { enqueueOutboxEvent } from '@/lib/core/outbox/service' +import type { DbOrTx } from '@/lib/db/types' +import { captureServerEvent } from '@/lib/posthog/server' + +const logger = createLogger('BillingCycleClose') + +/** + * Minimum residual overage worth invoicing at cycle close, in dollars. + * Anything below this is forgiven rather than billed as a sub-cent invoice. + */ +const MIN_CLOSE_INVOICE_DOLLARS = 0.5 + +/** + * Settlement grace after a rollover before its elapsed period may close. + * Billing attribution is frozen at run start (the payer is immutable for the + * run), so a run that started just before the rollover can insert rows + * stamped with the elapsed period after it ends. Closing only once the + * rollover is older than any possible in-flight run guarantees the close's + * ledger sums are final — no straggler row is orphaned from the final + * overage or bookkeeping. Non-enterprise execution timeouts are far below + * this bound; the sweep simply picks the period up on a later run. + */ +const CLOSE_SETTLEMENT_GRACE_MS = 60 * 60 * 1000 + +type SubscriptionRow = typeof subscriptionTable.$inferSelect + +export type CycleCloseStatus = 'initialized' | 'current' | 'closed' | 'already-closed' | 'skipped' + +export interface CycleCloseResult { + status: CycleCloseStatus + subscriptionId: string + overageBilled?: number + creditsApplied?: number +} + +/** + * Subtract one billing interval from a period boundary. Mirrors Stripe's + * anchor-day semantics closely enough for a close window: the ledger rows are + * matched by their write-time period stamps, so this bound only needs to + * enclose the closed period, not reproduce it exactly. + */ +function minusOneInterval(date: Date, billingInterval: string | null): Date { + const result = new Date(date.getTime()) + if (billingInterval === 'year') { + result.setUTCFullYear(result.getUTCFullYear() - 1) + } else { + result.setUTCMonth(result.getUTCMonth() - 1) + } + return result +} + +/** Order-insensitive membership fingerprint for under-lock roster revalidation. */ +function rosterSignature(rows: { userId: string; role: string }[]): string { + return rows + .map((row) => `${row.userId}:${row.role}`) + .sort() + .join('|') +} + +/** + * Whether this subscription's usage windows derive from an enterprise + * reporting anchor. Asks the same resolver the usage math uses, so a + * malformed anchor (hand-edited Stripe metadata) that the resolver rejects — + * falling back to Stripe bounds — is treated identically here: the ledger + * rows are stamped with Stripe windows, and the close books them normally. + */ +function usesReportingWindows(sub: { + plan?: string | null + billingInterval?: string | null + metadata?: unknown + periodStart?: Date | null + periodEnd?: Date | null +}): boolean { + return resolveSubscriptionUsagePeriod(sub)?.source === 'reporting' +} + +/** + * Whether a subscription's previous period has already been closed — i.e. the + * durable close marker has caught up to the current `periodStart`. + * + * Threshold billing gates on this so the shared `billedOverageThisPeriod` + * tracker never mixes periods: after a rollover but before the sweep closes + * the elapsed period, a new-period settlement would be subtracted from the + * elapsed period's final overage and then wiped by the close's tracker reset, + * under-billing one period and double-billing the other. Skipping settlement + * until the close lands (sweep cadence, ≤6h) removes the race; a null marker + * (pre-first-sweep) also gates, and a null `periodStart` cannot race at all. + * + * The same predicate revalidates inside the settlement transaction (pass the + * `tx` as `executor` plus the `expectedPeriodStart` the overage was computed + * against): the unlocked preflight leaves a window where a rollover and its + * close can commit first, so the settlement re-checks under the tracker lock + * and aborts when the period moved. + */ +export async function isSubscriptionCycleCloseCurrent( + subscriptionId: string, + options: { executor?: DbOrTx; expectedPeriodStart?: Date | null } = {} +): Promise { + const executor = options.executor ?? db + const [row] = await executor + .select({ + periodStart: subscriptionTable.periodStart, + lastClosedPeriodStart: subscriptionTable.lastClosedPeriodStart, + }) + .from(subscriptionTable) + .where(eq(subscriptionTable.id, subscriptionId)) + .limit(1) + + if (options.expectedPeriodStart) { + if (!row?.periodStart || row.periodStart.getTime() !== options.expectedPeriodStart.getTime()) { + return false + } + } else if (!row?.periodStart) { + return true + } + + return ( + row.lastClosedPeriodStart !== null && + row.lastClosedPeriodStart.getTime() >= row.periodStart.getTime() + ) +} + +/** + * Close any elapsed-but-unclosed billing period for a subscription that is + * being deleted, ahead of its terminal settlement. A deleted subscription + * leaves the sweep's candidate set (its status leaves + * `ENTITLED_SUBSCRIPTION_STATUSES`), so this is the last chance to settle a + * period the sweep has not caught up to — without it, `claimTerminalPeriod` + * would advance the marker past the elapsed period and silently forgive its + * final overage, while the elapsed period's threshold collections would + * wrongly offset the terminal window's. The settlement grace is bypassed: + * no later sweep will revisit, so straggler rows are forgiven exactly like + * the terminal settlement's own. + */ +export async function closeElapsedPeriodBeforeDeletion(subscriptionId: string): Promise { + const [row] = await db + .select() + .from(subscriptionTable) + .where(eq(subscriptionTable.id, subscriptionId)) + .limit(1) + if (!row?.periodStart) return + const lagging = + row.lastClosedPeriodStart === null || + row.lastClosedPeriodStart.getTime() < row.periodStart.getTime() + if (!lagging) return + + const result = await closeElapsedBillingPeriod(row, { bypassSettlementGrace: true }) + if (result.status === 'skipped') { + // The close deferred (missing Stripe linkage, ownerless org, or a roster + // change mid-close). Deletion proceeds — blocking member downgrades on an + // unbillable period is the wrong trade — so the residual overage is + // forgiven; the close path already logged the specific cause. + logger.error( + 'Deletion proceeding past an unclosable elapsed period; residual overage forgiven', + { + subscriptionId, + plan: row.plan, + marker: row.lastClosedPeriodStart?.toISOString() ?? null, + periodStart: row.periodStart.toISOString(), + } + ) + } +} + +/** + * Claim the terminal period for a subscription that is being deleted, BEFORE + * the deletion handler computes and charges final overage. Reads the + * subscription row fresh (webhook payloads can be stale across a rollover) + * and advances the close marker to its current `periodStart` in one + * transaction, serializing with the sweep on the subscription row: an + * in-flight sweep close then fails its guarded marker claim and rolls back — + * including its outbox invoice — so deletion and sweep can never both bill + * the same period. Call `closeElapsedPeriodBeforeDeletion` first so a lagging + * elapsed period is settled rather than jumped. Returns the fresh period + * bounds for the deletion flow to settle against, plus `markerWasCurrent`: + * whether the close marker had already caught up to the terminal period. + * The `billedOverageThisPeriod` tracker only ever holds collections for the + * period that began at the marker (the threshold gate blocks settlement + * whenever the marker lags), so the terminal settlement must ignore the + * tracker when the marker was still lagging — its contents belong to a + * forgiven elapsed period, never to the terminal window. + * + * A lagging marker means an elapsed period is still unclosed — either the + * preceding close deferred, or a rollover committed between that close and + * this claim. By default the claim then leaves the marker untouched so the + * caller can run the close again and re-claim; `sealLagging` advances the + * marker over the unclosed period anyway (logging the forgiveness), which + * also guarantees an in-flight sweep that selected this subscription before + * its status changed aborts its own conflicting close. + */ +export async function claimTerminalPeriod( + subscriptionId: string, + options: { sealLagging?: boolean } = {} +): Promise<{ + periodStart: Date | null + periodEnd: Date | null + markerWasCurrent: boolean +}> { + return db.transaction(async (tx) => { + const [row] = await tx + .select({ + periodStart: subscriptionTable.periodStart, + periodEnd: subscriptionTable.periodEnd, + lastClosedPeriodStart: subscriptionTable.lastClosedPeriodStart, + }) + .from(subscriptionTable) + .where(eq(subscriptionTable.id, subscriptionId)) + .for('update') + .limit(1) + + if (!row?.periodStart) { + // Mirrors the threshold gate: a null `periodStart` cannot race a + // rollover, so any tracked collections are legitimately current. + return { periodStart: null, periodEnd: null, markerWasCurrent: true } + } + const markerWasCurrent = + !!row.lastClosedPeriodStart && + row.lastClosedPeriodStart.getTime() >= row.periodStart.getTime() + if (!markerWasCurrent && options.sealLagging) { + logger.error( + 'Sealing an unclosed elapsed period at terminal claim; residual overage forgiven', + { + subscriptionId, + marker: row.lastClosedPeriodStart?.toISOString() ?? null, + periodStart: row.periodStart.toISOString(), + } + ) + await claimCloseMarker(tx, subscriptionId, row.periodStart) + } + return { periodStart: row.periodStart, periodEnd: row.periodEnd, markerWasCurrent } + }) +} + +/** + * Advance the durable close marker to `periodStart`, guarded so concurrent + * closers and replays collapse to one winner. Returns false when another + * worker already advanced the marker at or past this boundary. + */ +async function claimCloseMarker( + tx: Parameters[0]>[0], + subscriptionId: string, + periodStart: Date +): Promise { + const claimed = await tx + .update(subscriptionTable) + .set({ lastClosedPeriodStart: periodStart }) + .where( + and( + eq(subscriptionTable.id, subscriptionId), + or( + isNull(subscriptionTable.lastClosedPeriodStart), + lt(subscriptionTable.lastClosedPeriodStart, periodStart) + ) + ) + ) + .returning({ id: subscriptionTable.id }) + return claimed.length > 0 +} + +/** + * Close the most recently elapsed billing period for one subscription. + * + * Runs when the durable `lastClosedPeriodStart` marker lags the subscription's + * current `periodStart` (better-auth advances the row's period from Stripe's + * `customer.subscription.updated`). The close, per closed period: + * + * 1. Sums the closed period's ledger usage per member from write-time period + * stamps (`getStampedPeriodRangeUsageCostByUser`) — never `created_at`. + * 2. Collects final sub-threshold overage for non-enterprise plans: computed + * overage minus what threshold billing already collected + * (`billedOverageThisPeriod`), credits applied first, remainder invoiced + * through the transaction-enlisted Stripe outbox with deterministic + * idempotency stems keyed by `(subscriptionId, closed period)`. + * 3. Writes `lastPeriodCost` / `lastPeriodCopilotCost` bookkeeping from the + * same ledger sums and resets `billedOverageThisPeriod` for the new period. + * 4. Advances the marker in the same transaction, so the money, bookkeeping, + * and marker commit atomically — a crash retries the whole close, and the + * outbox's Stripe idempotency keys collapse invoice replays. + * + * Enterprise subscriptions never collect money here (billing is contractual, + * outside Stripe); they get bookkeeping + marker only, and orgs on reporting + * anchors skip bookkeeping too because their windows derive live from the + * anchor. A null marker initializes to the current `periodStart` without + * billing, so historical periods are never retroactively closed. + */ +export async function closeElapsedBillingPeriod( + sub: SubscriptionRow, + options: { bypassSettlementGrace?: boolean } = {} +): Promise { + const base: CycleCloseResult = { status: 'skipped', subscriptionId: sub.id } + + if (!sub.periodStart || isFree(sub.plan)) return base + const periodStart = sub.periodStart + + if (sub.lastClosedPeriodStart && sub.lastClosedPeriodStart.getTime() >= periodStart.getTime()) { + return { ...base, status: 'current' } + } + + if (!sub.lastClosedPeriodStart) { + await db + .update(subscriptionTable) + .set({ lastClosedPeriodStart: periodStart }) + .where(and(eq(subscriptionTable.id, sub.id), isNull(subscriptionTable.lastClosedPeriodStart))) + logger.info('Initialized cycle-close marker without billing', { + subscriptionId: sub.id, + plan: sub.plan, + periodStart: periodStart.toISOString(), + }) + return { ...base, status: 'initialized' } + } + + if ( + !options.bypassSettlementGrace && + Date.now() - periodStart.getTime() < CLOSE_SETTLEMENT_GRACE_MS + ) { + // Rollover too recent — a run started before it could still insert rows + // stamped with the elapsed period. A later sweep closes it with final sums. + return base + } + + const marker = sub.lastClosedPeriodStart + const orgScoped = await isSubscriptionOrgScoped(sub) + const billingEntity = orgScoped + ? ({ type: 'organization', id: sub.referenceId } as const) + : ({ type: 'user', id: sub.referenceId } as const) + // The elapsed period's exact start, from the ledger's own write-time stamps + // (its rows carry `billing_period_end == periodStart` — the renewal + // invariant). Deriving the bound from stamps instead of calendar math keeps + // the refresh window aligned with the stamped period even when anchor-day + // drift (e.g. Jan 31 → Feb 28) makes `periodStart - 1 interval` inexact. + const [prevStamp] = await db + .select({ + // mapWith(column) applies the timestamp decoder — a raw aggregate + // bypasses column mapping, so the driver would return a string here. + start: sql`max(${usageLog.billingPeriodStart})`.mapWith( + usageLog.billingPeriodStart + ), + }) + .from(usageLog) + .where( + and( + eq(usageLog.billingEntityType, billingEntity.type), + eq(usageLog.billingEntityId, billingEntity.id), + eq(usageLog.billingPeriodEnd, periodStart) + ) + ) + const expectedPrevStart = prevStamp?.start ?? minusOneInterval(periodStart, sub.billingInterval) + // Money and bookkeeping cover exactly one period. A marker further back + // than one interval means missed sweeps; those older periods' sub-threshold + // tails are forgiven (loudly) rather than billed with multi-period math. + const closeFrom = marker.getTime() < expectedPrevStart.getTime() ? expectedPrevStart : marker + if (closeFrom.getTime() !== marker.getTime()) { + logger.error('Cycle close skipped elapsed periods; forgiving their residual overage', { + subscriptionId: sub.id, + plan: sub.plan, + marker: marker.toISOString(), + closingFrom: closeFrom.toISOString(), + periodStart: periodStart.toISOString(), + }) + } + if (closeFrom.getTime() >= periodStart.getTime()) { + // Degenerate window (clock skew or a shortened period) — just advance. + const advanced = await db.transaction(async (tx) => claimCloseMarker(tx, sub.id, periodStart)) + return { ...base, status: advanced ? 'closed' : 'already-closed' } + } + + const closedRange = { from: closeFrom, to: periodStart } + + const enterprise = isEnterprise(sub.plan) + if (enterprise && usesReportingWindows(sub)) { + // Reporting-anchor orgs derive every usage window live from the anchor; + // there is nothing to bill or book here. Advance the marker so the sweep + // stays quiet. + const advanced = await db.transaction(async (tx) => claimCloseMarker(tx, sub.id, periodStart)) + return { ...base, status: advanced ? 'closed' : 'already-closed' } + } + + const [usageByUser, copilotByUser] = await Promise.all([ + getStampedPeriodRangeUsageCostByUser(billingEntity, closedRange), + getStampedPeriodRangeUsageCostByUser(billingEntity, closedRange, COPILOT_USAGE_SOURCES), + ]) + let closedLedgerUsage = 0 + for (const cost of usageByUser.values()) closedLedgerUsage += cost + + const memberRows = orgScoped + ? await db + .select({ userId: member.userId, role: member.role }) + .from(member) + .where(eq(member.organizationId, sub.referenceId)) + : [] + const memberIds = orgScoped ? memberRows.map((row) => row.userId) : [sub.referenceId] + const trackerUserId = orgScoped + ? (memberRows.find((row) => row.role === 'owner')?.userId ?? null) + : sub.referenceId + + // Final overage for the closed period (enterprise never bills overage). + // Refresh reads are scoped by the same entity/period stamps as the ledger + // sums, so a departed member's org-attributed rows offset the overage + // exactly like a current member's. + let totalOverage = 0 + if (!enterprise) { + if (orgScoped) { + const { totalOverage: computed } = await computeOrgOverageAmount({ + plan: sub.plan, + seats: sub.seats ?? null, + periodStart: closeFrom, + periodEnd: periodStart, + organizationId: sub.referenceId, + pooledLedgerUsage: closedLedgerUsage, + }) + totalOverage = computed + } else { + const planDollars = getPlanTierDollars(sub.plan) + let refreshConsumed = 0 + if (planDollars > 0) { + refreshConsumed = await computeDailyRefreshConsumed({ + billingEntity, + periodStart: closeFrom, + periodEnd: periodStart, + planDollars, + }) + } + const { basePrice } = getPlanPricing(sub.plan) + totalOverage = Math.max(0, closedLedgerUsage - refreshConsumed - basePrice) + } + } + + // Labels use the closed period's END month, matching threshold billing's + // period-end labeling for overage invoices. + const billingPeriodLabel = periodStart.toISOString().slice(0, 7) + const collectMoney = !enterprise && totalOverage > 0 + if (collectMoney && (!sub.stripeCustomerId || !sub.stripeSubscriptionId)) { + // Claiming the marker here would silently forgive the overage. Defer the + // whole close — the sweep retries every run until the Stripe linkage is + // repaired, and this error is the operator signal. + logger.error('Deferring cycle close: overage due but Stripe identifiers are missing', { + subscriptionId: sub.id, + plan: sub.plan, + totalOverage, + hasStripeCustomerId: !!sub.stripeCustomerId, + hasStripeSubscriptionId: !!sub.stripeSubscriptionId, + }) + return base + } + if (collectMoney && orgScoped && !trackerUserId) { + // Same defer as missing Stripe state: without an owner row there is no + // billed-overage tracker or credit target, and claiming the marker would + // silently forgive the overage. The sweep retries once ownership is + // repaired; mirrors threshold billing's missing-owner handling. + logger.error('Deferring cycle close: overage due but organization has no owner', { + subscriptionId: sub.id, + organizationId: sub.referenceId, + plan: sub.plan, + totalOverage, + }) + return base + } + + const closeResult = await db.transaction( + async ( + tx + ): Promise<{ + status: 'closed' | 'already-closed' | 'membership-changed' + billed: number + creditsApplied: number + }> => { + await tx.execute(sql.raw(`SET LOCAL lock_timeout = '${BILLING_LOCK_TIMEOUT_MS}ms'`)) + + // Canonical lock order: member userStats rows, then the organization row. + if (memberIds.length > 0) { + await tx + .select({ userId: userStats.userId }) + .from(userStats) + .where(inArray(userStats.userId, memberIds)) + .for('update') + } + let orgCreditBalance = 0 + if (orgScoped) { + const [orgRow] = await tx + .select({ creditBalance: organization.creditBalance }) + .from(organization) + .where(eq(organization.id, sub.referenceId)) + .for('update') + .limit(1) + orgCreditBalance = toNumber(toDecimal(orgRow?.creditBalance)) + } + + // Re-check the marker under the locks: a concurrent closer that already + // committed makes this a no-op (its billedOverage reset must not be + // mistaken for unbilled overage). + const [current] = await tx + .select({ lastClosedPeriodStart: subscriptionTable.lastClosedPeriodStart }) + .from(subscriptionTable) + .where(eq(subscriptionTable.id, sub.id)) + .limit(1) + if ( + current?.lastClosedPeriodStart && + current.lastClosedPeriodStart.getTime() >= periodStart.getTime() + ) { + return { status: 'already-closed', billed: 0, creditsApplied: 0 } + } + + // Re-read the roster under the locks, mirroring threshold billing: an + // owner transfer moves `billedOverageThisPeriod` between rows, so a + // roster read from before the locks could settle against the wrong + // tracker or reset a stale member set. The sweep simply retries. + if (orgScoped) { + const lockedRoster = await tx + .select({ userId: member.userId, role: member.role }) + .from(member) + .where(eq(member.organizationId, sub.referenceId)) + if (rosterSignature(lockedRoster) !== rosterSignature(memberRows)) { + return { status: 'membership-changed', billed: 0, creditsApplied: 0 } + } + } + + let billed = 0 + let creditsApplied = 0 + + if (collectMoney && trackerUserId) { + const [tracker] = await tx + .select({ + billedOverageThisPeriod: userStats.billedOverageThisPeriod, + creditBalance: userStats.creditBalance, + }) + .from(userStats) + .where(eq(userStats.userId, trackerUserId)) + .limit(1) + + // The tracker's collections belong to the period that began at the + // marker — the threshold gate blocks settlement whenever the marker + // lags, so nothing newer can be in it. When this close skipped + // forgiven periods (`closeFrom` advanced past the marker), those + // collections offset a forgiven period's overage, not this one's: + // count nothing against this close. The reset below still clears them. + const alreadyBilled = + closeFrom.getTime() === marker.getTime() + ? toNumber(toDecimal(tracker?.billedOverageThisPeriod)) + : 0 + let remaining = Math.max(0, totalOverage - alreadyBilled) + + if (remaining > 0) { + const creditBalance = orgScoped + ? orgCreditBalance + : toNumber(toDecimal(tracker?.creditBalance)) + if (creditBalance > 0) { + creditsApplied = Math.min(creditBalance, remaining) + if (orgScoped) { + await tx + .update(organization) + .set({ + creditBalance: sql`GREATEST(0, ${organization.creditBalance} - ${creditsApplied})`, + }) + .where(eq(organization.id, sub.referenceId)) + } else { + await tx + .update(userStats) + .set({ + creditBalance: sql`GREATEST(0, ${userStats.creditBalance} - ${creditsApplied})`, + }) + .where(eq(userStats.userId, trackerUserId)) + } + remaining -= creditsApplied + } + + if (remaining >= MIN_CLOSE_INVOICE_DOLLARS) { + const amountCents = Math.round(remaining * 100) + const idemStem = `cycle-close-overage:${sub.id}:${periodStart.toISOString()}` + await enqueueOutboxEvent(tx, OUTBOX_EVENT_TYPES.STRIPE_THRESHOLD_OVERAGE_INVOICE, { + customerId: sub.stripeCustomerId, + stripeSubscriptionId: sub.stripeSubscriptionId, + amountCents, + description: `Final overage billing – ${billingPeriodLabel}`, + itemDescription: `Usage overage ($${remaining.toFixed(2)})`, + billingPeriod: billingPeriodLabel, + invoiceIdemKeyStem: `${idemStem}:invoice`, + itemIdemKeyStem: `${idemStem}:item`, + metadata: { + type: 'overage_billing', + subscriptionId: sub.stripeSubscriptionId ?? '', + billingPeriod: billingPeriodLabel, + ...(orgScoped ? { organizationId: sub.referenceId } : { userId: sub.referenceId }), + }, + }) + billed = remaining + } else if (remaining > 0) { + logger.info('Forgiving sub-minimum cycle-close overage', { + subscriptionId: sub.id, + remaining, + }) + } + } + } + + // Bookkeeping: previous-period totals from the same stamped ledger sums. + if (memberIds.length > 0) { + const lastCostCases = sql.join( + memberIds.map( + (userId) => sql`WHEN ${userId} THEN ${(usageByUser.get(userId) ?? 0).toString()}` + ), + sql` ` + ) + const lastCopilotCases = sql.join( + memberIds.map( + (userId) => sql`WHEN ${userId} THEN ${(copilotByUser.get(userId) ?? 0).toString()}` + ), + sql` ` + ) + await tx + .update(userStats) + .set({ + lastPeriodCost: sql`CASE ${userStats.userId} ${lastCostCases} ELSE ${userStats.lastPeriodCost} END`, + lastPeriodCopilotCost: sql`CASE ${userStats.userId} ${lastCopilotCases} ELSE ${userStats.lastPeriodCopilotCost} END`, + billedOverageThisPeriod: '0', + }) + .where(inArray(userStats.userId, memberIds)) + } + if (orgScoped) { + await tx + .update(organization) + .set({ departedMemberUsage: '0' }) + .where(eq(organization.id, sub.referenceId)) + } + + const advanced = await claimCloseMarker(tx, sub.id, periodStart) + if (!advanced) { + throw new Error( + `Cycle-close marker for subscription ${sub.id} advanced concurrently; rolling back` + ) + } + + return { status: 'closed', billed, creditsApplied } + } + ) + + if (closeResult.status === 'already-closed') { + return { ...base, status: 'already-closed' } + } + if (closeResult.status === 'membership-changed') { + logger.info('Deferring cycle close: organization membership changed mid-close', { + subscriptionId: sub.id, + organizationId: sub.referenceId, + }) + return base + } + + logger.info('Closed billing period', { + subscriptionId: sub.id, + plan: sub.plan, + orgScoped, + closedFrom: closeFrom.toISOString(), + closedTo: periodStart.toISOString(), + closedLedgerUsage, + totalOverage, + overageBilled: closeResult.billed, + creditsApplied: closeResult.creditsApplied, + }) + + if (closeResult.billed > 0 || closeResult.creditsApplied > 0) { + const actorId = trackerUserId ?? sub.referenceId + const settledVia = closeResult.billed > 0 ? 'stripe' : 'credits' + recordAudit({ + actorId, + action: AuditAction.OVERAGE_BILLED, + resourceType: AuditResourceType.BILLING, + resourceId: sub.id, + description: `Final overage of $${(closeResult.billed + closeResult.creditsApplied).toFixed(2)} settled at cycle close for ${sub.referenceId}`, + metadata: { + entityType: billingEntity.type, + referenceId: sub.referenceId, + ...(orgScoped ? { organizationId: sub.referenceId } : {}), + plan: sub.plan, + amount: closeResult.billed + closeResult.creditsApplied, + currency: 'usd', + creditsApplied: closeResult.creditsApplied, + settledVia, + billingPeriod: billingPeriodLabel, + }, + }) + captureServerEvent(actorId, 'overage_billed', { + amount: closeResult.billed + closeResult.creditsApplied, + currency: 'usd', + entity_type: billingEntity.type, + reference_id: sub.referenceId, + settled_via: settledVia, + }) + } + + return { + ...base, + status: 'closed', + overageBilled: closeResult.billed, + creditsApplied: closeResult.creditsApplied, + } +} + +/** + * Terminal bookkeeping for a subscription that is ending (deleted/cancelled): + * writes `lastPeriodCost` / `lastPeriodCopilotCost` from the final period's + * stamped ledger sums, clears the per-period trackers so a future + * subscription starts clean. Money is NOT collected here — the deletion + * handler claims the terminal period via `claimTerminalPeriod` and bills the + * final overage itself before calling this. + * + * Reporting-anchor enterprise subscriptions are skipped: their usage windows + * derive live from the anchor, and the subscription's Stripe bounds would + * range the wrong stamped rows. + */ +export async function writeFinalPeriodBookkeeping(sub: { + id: string + plan: string | null + referenceId: string + billingInterval?: string | null + periodStart?: Date | null + periodEnd?: Date | null + metadata?: unknown +}): Promise { + if (!sub.periodStart) return + const periodStart = sub.periodStart + + if (usesReportingWindows(sub)) return + + const orgScoped = await isSubscriptionOrgScoped(sub) + const billingEntity = orgScoped + ? ({ type: 'organization', id: sub.referenceId } as const) + : ({ type: 'user', id: sub.referenceId } as const) + const range = { from: periodStart, to: sub.periodEnd ?? new Date() } + + const [usageByUser, copilotByUser] = await Promise.all([ + getStampedPeriodRangeUsageCostByUser(billingEntity, range), + getStampedPeriodRangeUsageCostByUser(billingEntity, range, COPILOT_USAGE_SOURCES), + ]) + + const memberIds = orgScoped + ? ( + await db + .select({ userId: member.userId }) + .from(member) + .where(eq(member.organizationId, sub.referenceId)) + ).map((row) => row.userId) + : [sub.referenceId] + + await db.transaction(async (tx) => { + await tx.execute(sql.raw(`SET LOCAL lock_timeout = '${BILLING_LOCK_TIMEOUT_MS}ms'`)) + if (memberIds.length > 0) { + const lastCostCases = sql.join( + memberIds.map( + (userId) => sql`WHEN ${userId} THEN ${(usageByUser.get(userId) ?? 0).toString()}` + ), + sql` ` + ) + const lastCopilotCases = sql.join( + memberIds.map( + (userId) => sql`WHEN ${userId} THEN ${(copilotByUser.get(userId) ?? 0).toString()}` + ), + sql` ` + ) + await tx + .update(userStats) + .set({ + lastPeriodCost: sql`CASE ${userStats.userId} ${lastCostCases} ELSE ${userStats.lastPeriodCost} END`, + lastPeriodCopilotCost: sql`CASE ${userStats.userId} ${lastCopilotCases} ELSE ${userStats.lastPeriodCopilotCost} END`, + billedOverageThisPeriod: '0', + }) + .where(inArray(userStats.userId, memberIds)) + } + if (orgScoped) { + await tx + .update(organization) + .set({ departedMemberUsage: '0' }) + .where(eq(organization.id, sub.referenceId)) + } + }) +} + +export interface CycleCloseSweepSummary { + candidates: number + closed: number + initialized: number + failed: number +} + +/** + * Daily catch-all that closes every elapsed billing period. Candidates are + * entitled subscriptions whose close marker lags their current `periodStart` + * — i.e. the period advanced (via Stripe sync) since the last close. Each + * close is independently atomic, so one failure never blocks the rest. + */ +export async function sweepBillingCycleCloses(): Promise { + const candidates = await db + .select() + .from(subscriptionTable) + .where( + and( + inArray(subscriptionTable.status, ENTITLED_SUBSCRIPTION_STATUSES), + sql`${subscriptionTable.periodStart} IS NOT NULL`, + or( + isNull(subscriptionTable.lastClosedPeriodStart), + lt(subscriptionTable.lastClosedPeriodStart, subscriptionTable.periodStart) + ) + ) + ) + + const summary: CycleCloseSweepSummary = { + candidates: candidates.length, + closed: 0, + initialized: 0, + failed: 0, + } + + for (const sub of candidates) { + try { + const result = await closeElapsedBillingPeriod(sub) + if (result.status === 'closed') summary.closed++ + if (result.status === 'initialized') summary.initialized++ + } catch (error) { + summary.failed++ + logger.error('Cycle close failed for subscription', { + subscriptionId: sub.id, + plan: sub.plan, + error: getErrorMessage(error), + }) + } + } + + logger.info('Billing cycle-close sweep finished', { ...summary }) + return summary +} diff --git a/apps/sim/lib/billing/organizations/lock-order.test.ts b/apps/sim/lib/billing/organizations/lock-order.test.ts index fb1d96893ae..1f25db893e7 100644 --- a/apps/sim/lib/billing/organizations/lock-order.test.ts +++ b/apps/sim/lib/billing/organizations/lock-order.test.ts @@ -111,17 +111,21 @@ describe('paid-org join billing lock ordering', () => { mockChangeWorkspaceStoragePayersInTx.mockReset() }) - it('locks the personal subscription before mutating userStats', async () => { + it('locks the personal subscription before pausing it and never mutates userStats', async () => { const { tx, ops } = createRecordingTx() await reapplyPaidOrgJoinBillingForExistingMemberTx(tx as DbOrTx, 'user-1', 'org-1') - const firstUserStatsUpdate = ops.findIndex((o) => o.op === 'update' && o.table === userStats) + const userStatsUpdate = ops.findIndex((o) => o.op === 'update' && o.table === userStats) const subscriptionLock = ops.findIndex((o) => o.op === 'lock' && o.table === subscriptionTable) + const subscriptionUpdate = ops.findIndex( + (o) => o.op === 'update' && o.table === subscriptionTable + ) - expect(firstUserStatsUpdate).toBeGreaterThanOrEqual(0) + // Ledger entity stamps attribute usage; join billing no longer touches userStats. + expect(userStatsUpdate).toBe(-1) expect(subscriptionLock).toBeGreaterThanOrEqual(0) - expect(subscriptionLock).toBeLessThan(firstUserStatsUpdate) + expect(subscriptionUpdate).toBeGreaterThan(subscriptionLock) }) it('still locks an already-paused personal Pro so a concurrent restore cannot pass it', async () => { @@ -276,8 +280,9 @@ describe('workspace payer-change transaction lock ordering', () => { ) const payerTransfer = ops.findIndex((entry) => entry.op === 'payer-transfer') expect(workspaceLock).toBeGreaterThanOrEqual(0) - expect(userStatsUpdate).toBeGreaterThan(workspaceLock) - expect(payerTransfer).toBeGreaterThan(userStatsUpdate) + // Ledger entity stamps attribute usage; join billing no longer touches userStats. + expect(userStatsUpdate).toBe(-1) + expect(payerTransfer).toBeGreaterThan(workspaceLock) }) }) diff --git a/apps/sim/lib/billing/organizations/membership.ts b/apps/sim/lib/billing/organizations/membership.ts index bbc479963fd..e25c96c8822 100644 --- a/apps/sim/lib/billing/organizations/membership.ts +++ b/apps/sim/lib/billing/organizations/membership.ts @@ -206,20 +206,14 @@ export interface RestoreProResult { /** * Restore a user's personal Pro subscription if it was paused - * (`cancelAtPeriodEnd = true`) and merge any snapshotted Pro usage back - * into their current-period usage. + * (`cancelAtPeriodEnd = true`). No usage moves — ledger entity stamps kept + * their personal usage attributed to them throughout the org membership. * - * All DB mutations run inside a single transaction so partial progress - * cannot be committed: either both the subscription un-pause and the - * usage snapshot merge succeed, or neither does. Errors propagate to - * the caller so webhook handlers can rely on Stripe retry semantics. + * Errors propagate to the caller so webhook handlers can rely on Stripe + * retry semantics. * - * Idempotent: - * - Early returns when the user has no paused Pro subscription, so - * re-runs after a successful restore are no-ops. - * - The snapshot merge only runs when `proPeriodCostSnapshot > 0`, - * so a second call after a prior success (which zeroes the - * snapshot) does nothing. + * Idempotent: early returns when the user has no paused Pro subscription, + * so re-runs after a successful restore are no-ops. * * Called when: * - A member leaves a team (via `removeUserFromOrganization`). @@ -287,46 +281,6 @@ export async function restoreUserProSubscription(userId: string): Promise 0) { - await tx - .update(organization) - .set({ - departedMemberUsage: sql`${organization.departedMemberUsage} + ${usageCaptured}`, - }) - .where(eq(organization.id, params.sourceOrganizationId)) - await tx - .update(userStats) - .set({ currentPeriodCost: '0' }) - .where(eq(userStats.userId, params.userId)) - } - const added = await ensureUserInOrganizationTx(tx, { userId: params.userId, organizationId: params.destinationOrganizationId, @@ -1279,7 +1185,9 @@ export async function transferUserBetweenOrganizations( workspaceAccessRevoked, credentialMembershipsRevoked, pendingInvitationsCancelled: cancelledInvitations.length, - usageCaptured, + // Nothing to capture: the member's ledger rows stay stamped to the + // source org's period and are billed at its cycle close. + usageCaptured: 0, } } ) @@ -1299,10 +1207,11 @@ export async function transferUserBetweenOrganizations( * * Handles: * - Owner removal prevention - * - Departed member usage capture * - Member record deletion * - Pro subscription restoration when leaving a paid team - * - Pro usage restoration from snapshot + * + * No usage moves on departure: the member's ledger rows stay stamped to the + * org's billing period and are billed at its cycle close. * * Note: Users can only belong to one organization at a time. */ @@ -1397,34 +1306,6 @@ export async function removeUserFromOrganization( .returning({ id: invitation.id }) : [] - const captureDepartedUsage = async () => { - if (skipBillingLogic) return 0 - - const [departingUserStats] = await tx - .select({ currentPeriodCost: userStats.currentPeriodCost }) - .from(userStats) - .where(eq(userStats.userId, userId)) - .for('update') - .limit(1) - - const usage = toNumber(toDecimal(departingUserStats?.currentPeriodCost)) - if (usage <= 0) return 0 - - await tx - .update(organization) - .set({ - departedMemberUsage: sql`${organization.departedMemberUsage} + ${usage}`, - }) - .where(eq(organization.id, organizationId)) - - await tx - .update(userStats) - .set({ currentPeriodCost: '0' }) - .where(eq(userStats.userId, userId)) - - return usage - } - // Permission groups are organization-scoped, so a departing member's group // membership must be cleared whenever they leave the org — including the // zero-workspace early return below (a group can exist with members but no @@ -1439,12 +1320,12 @@ export async function removeUserFromOrganization( ) if (workspaceIds.length === 0) { - const capturedUsage = await captureDepartedUsage() - return { skipped: false as const, workspaceIdsToRevoke: [] as string[], - usageCaptured: capturedUsage, + // Nothing to capture: the member's ledger rows stay stamped to + // this org's period and are billed at its cycle close. + usageCaptured: 0, credentialMembershipsRevoked: 0, pendingInvitationsCancelled: cancelledInvitations.length, } @@ -1484,12 +1365,11 @@ export async function removeUserFromOrganization( userId ) await removeWorkspaceSkillMembershipsTx(tx, workspaceIds, userId) - const capturedUsage = await captureDepartedUsage() return { skipped: false as const, workspaceIdsToRevoke: deletedPerms.map((row) => row.entityId), - usageCaptured: capturedUsage, + usageCaptured: 0, credentialMembershipsRevoked, pendingInvitationsCancelled: cancelledInvitations.length, } @@ -1513,14 +1393,6 @@ export async function removeUserFromOrganization( // resolving to this org immediately, not after the membership-cache TTL. invalidateMembershipCache(userId) - if (result.usageCaptured > 0) { - logger.info('Captured departed member usage', { - organizationId, - userId, - usage: result.usageCaptured, - }) - } - logger.info('Removed member from organization', { organizationId, userId, diff --git a/apps/sim/lib/billing/threshold-billing.test.ts b/apps/sim/lib/billing/threshold-billing.test.ts index c984f0c8038..cfab2c6016f 100644 --- a/apps/sim/lib/billing/threshold-billing.test.ts +++ b/apps/sim/lib/billing/threshold-billing.test.ts @@ -17,6 +17,7 @@ const { mockIsFree, mockIsOrgScopedSubscription, mockIsOrganizationBillingBlocked, + mockIsSubscriptionCycleCloseCurrent, mockRecordAudit, mockCaptureServerEvent, } = vi.hoisted(() => ({ @@ -32,6 +33,7 @@ const { mockIsFree: vi.fn(), mockIsOrgScopedSubscription: vi.fn(), mockIsOrganizationBillingBlocked: vi.fn(), + mockIsSubscriptionCycleCloseCurrent: vi.fn(), mockRecordAudit: vi.fn(), mockCaptureServerEvent: vi.fn(), })) @@ -61,6 +63,10 @@ vi.mock('@/lib/billing/core/usage-log', () => ({ getBillingPeriodUsageCost: mockGetBillingPeriodUsageCost, })) +vi.mock('@/lib/billing/cycle-close', () => ({ + isSubscriptionCycleCloseCurrent: mockIsSubscriptionCycleCloseCurrent, +})) + vi.mock('@/lib/billing/plan-helpers', () => ({ isEnterprise: mockIsEnterprise, isFree: mockIsFree, @@ -107,29 +113,14 @@ const expectedBillingPeriod = { end: new Date('2026-06-01T00:00:00.000Z'), } -const defaultUsageSnapshotRow = { - currentPeriodCost: '0', - proPeriodCostSnapshot: '0', - proPeriodCostSnapshotAt: null as Date | null, - lastPeriodCost: '0', -} - -/** - * Queues the two pre-transaction personal reads: the user_stats usage snapshot - * and the subscription's Stripe customer row. - */ -function queuePersonalReads( - snapshot: Record = defaultUsageSnapshotRow, - customerId = 'cus_1' -) { - queueTableRows(schemaMock.userStats, [snapshot]) +/** Queues the pre-transaction personal read: the subscription's Stripe customer row. */ +function queuePersonalReads(customerId = 'cus_1') { queueTableRows(schemaMock.subscription, [{ stripeCustomerId: customerId }]) } /** Builds the locked in-transaction user_stats row. */ function lockedStatsRow(overrides: Record = {}) { return { - ...defaultUsageSnapshotRow, billedOverageThisPeriod: '0', creditBalance: '0', ...overrides, @@ -144,20 +135,18 @@ function queueLockedStats(row: Record) { const orgMemberUsageRow = { userId: 'owner-1', role: 'owner', - currentPeriodCost: '350', - departedMemberUsage: '25', } /** * Queues the organization settlement reads in table order: the pre-transaction - * member usage join, then the locked owner row, owner stats, organization row, - * and locked member usage join inside the transaction. + * member join, then the locked owner row, owner stats, organization row, and + * locked member join inside the transaction. */ function queueOrgReads({ memberUsageRows = [orgMemberUsageRow], lockedOwnerRows = [{ userId: 'owner-1' }], ownerStatsRows = [{ billedOverageThisPeriod: '0' }], - organizationRows = [{ creditBalance: '0', departedMemberUsage: '25' }], + organizationRows = [{ creditBalance: '0' }], lockedMemberUsageRows = memberUsageRows, }: { memberUsageRows?: unknown[] @@ -174,6 +163,7 @@ function queueOrgReads({ } const usableOrgSubscription = { + id: 'sub-db-team-1', plan: 'team', seats: 2, periodStart: new Date('2026-05-01T00:00:00.000Z'), @@ -195,6 +185,7 @@ describe('checkAndBillOverageThreshold', () => { mockIsEnterprise.mockReturnValue(false) mockIsOrgScopedSubscription.mockReturnValue(false) mockGetBillingPeriodUsageCost.mockResolvedValue(0) + mockIsSubscriptionCycleCloseCurrent.mockResolvedValue(true) }) afterAll(() => { @@ -202,7 +193,6 @@ describe('checkAndBillOverageThreshold', () => { }) it('does not lock user_stats when calculated overage is below threshold', async () => { - queuePersonalReads() mockCalculateSubscriptionOverage.mockResolvedValue(99) await checkAndBillOverageThreshold('user-1') @@ -216,7 +206,7 @@ describe('checkAndBillOverageThreshold', () => { periodEnd: userSubscription.periodEnd, }) expect(dbChainMockFns.transaction).not.toHaveBeenCalled() - expect(dbChainMockFns.select).toHaveBeenCalledTimes(1) + expect(dbChainMockFns.select).not.toHaveBeenCalled() expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() }) @@ -540,22 +530,12 @@ describe('checkAndBillOverageThreshold', () => { expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() }) - it('skips personal threshold billing when locked usage inputs changed', async () => { - queuePersonalReads({ ...defaultUsageSnapshotRow, currentPeriodCost: '250' }) - queueLockedStats(lockedStatsRow({ lastPeriodCost: '250' })) - mockCalculateSubscriptionOverage.mockResolvedValue(250) - - await checkAndBillOverageThreshold('user-1') - - expect(dbChainMockFns.transaction).toHaveBeenCalled() - expect(dbChainMockFns.update).not.toHaveBeenCalled() - expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() - }) - - it('throws retryably in markerless strict mode when locked personal usage changes', async () => { - queuePersonalReads({ ...defaultUsageSnapshotRow, currentPeriodCost: '250' }) - queueLockedStats(lockedStatsRow({ lastPeriodCost: '250' })) + it('aborts settlement when the period advances between preflight and the locked transaction', async () => { + queuePersonalReads() + queueLockedStats(lockedStatsRow()) mockCalculateSubscriptionOverage.mockResolvedValue(250) + // Preflight passes; the under-lock revalidation sees the rollover. + mockIsSubscriptionCycleCloseCurrent.mockResolvedValueOnce(true).mockResolvedValueOnce(false) await expect( checkAndBillOverageThreshold('user-1', undefined, { onError: 'throw' }) @@ -564,10 +544,51 @@ describe('checkAndBillOverageThreshold', () => { code: 'concurrent_state_change', retryable: true, }) + + expect(mockIsSubscriptionCycleCloseCurrent).toHaveBeenLastCalledWith( + userSubscription.id, + expect.objectContaining({ expectedPeriodStart: userSubscription.periodStart }) + ) expect(dbChainMockFns.update).not.toHaveBeenCalled() expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() }) + it('defers personal settlement while the previous period cycle close is pending', async () => { + mockIsSubscriptionCycleCloseCurrent.mockResolvedValue(false) + mockCalculateSubscriptionOverage.mockResolvedValue(250) + + await expect( + checkAndBillOverageThreshold('user-1', undefined, { + onError: 'throw', + expectedBillingPeriod, + }) + ).resolves.toEqual({ status: 'no-op', reason: 'pending-cycle-close' }) + + expect(mockIsSubscriptionCycleCloseCurrent).toHaveBeenCalledWith(userSubscription.id) + expect(mockCalculateSubscriptionOverage).not.toHaveBeenCalled() + expect(dbChainMockFns.transaction).not.toHaveBeenCalled() + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + }) + + it('defers organization settlement while the previous period cycle close is pending', async () => { + mockIsOrgScopedSubscription.mockReturnValue(true) + mockIsOrganizationBillingBlocked.mockResolvedValue(false) + mockGetOrganizationSubscriptionUsable.mockResolvedValue(usableOrgSubscription) + mockIsSubscriptionCycleCloseCurrent.mockResolvedValue(false) + + await expect( + checkAndBillOverageThreshold('user-1', undefined, { + onError: 'throw', + expectedBillingPeriod, + }) + ).resolves.toEqual({ status: 'no-op', reason: 'pending-cycle-close' }) + + expect(mockIsSubscriptionCycleCloseCurrent).toHaveBeenCalledWith(usableOrgSubscription.id) + expect(mockComputeOrgOverageAmount).not.toHaveBeenCalled() + expect(dbChainMockFns.transaction).not.toHaveBeenCalled() + expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() + }) + it('wraps lock timeouts in markerless strict mode', async () => { queuePersonalReads() mockCalculateSubscriptionOverage.mockResolvedValue(250) @@ -588,6 +609,8 @@ describe('checkAndBillOverageThreshold', () => { mockIsOrgScopedSubscription.mockReturnValue(true) mockIsOrganizationBillingBlocked.mockResolvedValue(false) mockGetOrganizationSubscriptionUsable.mockResolvedValue(usableOrgSubscription) + // Pooled entity read — departed members' org-stamped rows are already in. + mockGetBillingPeriodUsageCost.mockResolvedValue(350) queueOrgReads() mockComputeOrgOverageAmount.mockResolvedValue({ totalOverage: 250, @@ -603,9 +626,7 @@ describe('checkAndBillOverageThreshold', () => { periodStart: new Date('2026-05-01T00:00:00.000Z'), periodEnd: new Date('2026-06-01T00:00:00.000Z'), organizationId: userSubscription.referenceId, - pooledCurrentPeriodCost: 350, - departedMemberUsage: 25, - memberIds: ['owner-1'], + pooledLedgerUsage: 350, }) expect(dbChainMockFns.transaction).toHaveBeenCalled() expect(mockComputeOrgOverageAmount.mock.invocationCallOrder[0]).toBeLessThan( @@ -615,27 +636,6 @@ describe('checkAndBillOverageThreshold', () => { expect(mockEnqueueOutboxEvent).toHaveBeenCalledTimes(1) }) - it('skips stale organization overage when locked usage inputs changed', async () => { - mockIsOrgScopedSubscription.mockReturnValue(true) - mockIsOrganizationBillingBlocked.mockResolvedValue(false) - mockGetOrganizationSubscriptionUsable.mockResolvedValue(usableOrgSubscription) - queueOrgReads({ - organizationRows: [{ creditBalance: '0', departedMemberUsage: '75' }], - lockedMemberUsageRows: [{ ...orgMemberUsageRow, departedMemberUsage: '75' }], - }) - mockComputeOrgOverageAmount.mockResolvedValue({ - totalOverage: 250, - baseSubscriptionAmount: 100, - effectiveUsage: 350, - }) - - await checkAndBillOverageThreshold('user-1') - - expect(dbChainMockFns.transaction).toHaveBeenCalled() - expect(mockEnqueueOutboxEvent).not.toHaveBeenCalled() - expect(dbChainMockFns.update).not.toHaveBeenCalled() - }) - it('rechecks organization billed overage on the locked owner tracker', async () => { mockIsOrgScopedSubscription.mockReturnValue(true) mockIsOrganizationBillingBlocked.mockResolvedValue(false) @@ -659,29 +659,11 @@ describe('checkAndBillOverageThreshold', () => { mockIsOrganizationBillingBlocked.mockResolvedValue(false) mockGetOrganizationSubscriptionUsable.mockResolvedValue(usableOrgSubscription) queueOrgReads({ - memberUsageRows: [ - orgMemberUsageRow, - { - userId: 'member-1', - role: 'member', - currentPeriodCost: '25', - departedMemberUsage: '25', - }, - ], + memberUsageRows: [orgMemberUsageRow, { userId: 'member-1', role: 'member' }], lockedOwnerRows: [{ userId: 'member-1' }], lockedMemberUsageRows: [ - { - userId: 'owner-1', - role: 'member', - currentPeriodCost: '350', - departedMemberUsage: '25', - }, - { - userId: 'member-1', - role: 'owner', - currentPeriodCost: '25', - departedMemberUsage: '25', - }, + { userId: 'owner-1', role: 'member' }, + { userId: 'member-1', role: 'owner' }, ], }) mockComputeOrgOverageAmount.mockResolvedValue({ diff --git a/apps/sim/lib/billing/threshold-billing.ts b/apps/sim/lib/billing/threshold-billing.ts index f99f821a110..47b660880e0 100644 --- a/apps/sim/lib/billing/threshold-billing.ts +++ b/apps/sim/lib/billing/threshold-billing.ts @@ -14,6 +14,7 @@ import { getOrganizationSubscriptionUsable, } from '@/lib/billing/core/subscription' import { type BillingEntity, getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' +import { isSubscriptionCycleCloseCurrent } from '@/lib/billing/cycle-close' import { isEnterprise, isFree } from '@/lib/billing/plan-helpers' import { hasUsableSubscriptionAccess, @@ -30,19 +31,9 @@ const logger = createLogger('ThresholdBilling') const OVERAGE_THRESHOLD = envNumber(env.OVERAGE_THRESHOLD_DOLLARS, DEFAULT_OVERAGE_THRESHOLD) const USAGE_TOTAL_EPSILON = 0.000001 -interface PersonalUsageSnapshot { - currentPeriodCost: number - proPeriodCostSnapshot: number - proPeriodCostSnapshotAt: Date | null - lastPeriodCost: number -} - interface OrganizationUsageSnapshot { - memberIds: string[] ownerId: string memberSignature: string - pooledCurrentPeriodCost: number - departedMemberUsage: number } interface ThresholdBillingPeriod { @@ -62,6 +53,7 @@ export type ThresholdSettlementNoOpReason = | 'billing-blocked' | 'billing-ineligible' | 'no-subscription' + | 'pending-cycle-close' | 'plan-ineligible' export type ThresholdSettlementOutcome = @@ -288,10 +280,13 @@ export async function checkAndBillOverageThreshold( return checkAndBillOrganizationOverageThreshold(userSubscription.referenceId, options) } - const usageSnapshot = await getPersonalUsageSnapshot(userId) - if (!usageSnapshot) { - logger.warn('User stats not found for threshold billing', { userId }) - return requireSettlementStateOutcome(options, 'User stats are required for settlement') + // Defer settlement while the previous period's cycle close is pending so + // `billedOverageThisPeriod` never mixes periods (see + // `isSubscriptionCycleCloseCurrent`). The sweep closes it within hours and + // a later threshold attempt settles normally. + if (!(await isSubscriptionCycleCloseCurrent(userSubscription.id))) { + logger.debug('Previous period cycle close pending; deferring threshold billing', { userId }) + return noOp(options, 'pending-cycle-close') } const currentOverage = await calculateSubscriptionOverage({ @@ -358,20 +353,27 @@ export async function checkAndBillOverageThreshold( return requireSettlementState(options, 'User stats are required for settlement') } - const stats = statsRecords[0] - const lockedUsageSnapshot = personalUsageSnapshotFromStats(stats) - if (!personalUsageSnapshotMatches(usageSnapshot, lockedUsageSnapshot)) { - logger.debug('Personal usage changed during threshold billing check; retry later', { + // Revalidate the preflight gate under the tracker lock: a rollover and + // its cycle close (which resets `billedOverageThisPeriod`) can commit + // between the unlocked check and this transaction, and a settlement + // computed from the elapsed period must not land on the new period's + // tracker. + if ( + !(await isSubscriptionCycleCloseCurrent(userSubscription.id, { + executor: tx, + expectedPeriodStart: userSubscription.periodStart, + })) + ) { + logger.debug('Subscription period advanced during threshold settlement; retry later', { userId, - usageSnapshot, - lockedUsageSnapshot, }) return retryConcurrentSettlement( options, - 'Personal usage changed during threshold settlement' + 'Subscription period advanced during threshold settlement' ) } + const stats = statsRecords[0] const billedOverageThisPeriod = toNumber(toDecimal(stats.billedOverageThisPeriod)) const unbilledOverage = Math.max(0, currentOverage - billedOverageThisPeriod) @@ -558,6 +560,17 @@ async function checkAndBillOrganizationOverageThreshold( return noOp(options, 'billing-blocked') } + // Defer settlement while the previous period's cycle close is pending so + // `billedOverageThisPeriod` never mixes periods (see + // `isSubscriptionCycleCloseCurrent`). The sweep closes it within hours and + // a later threshold attempt settles normally. + if (!(await isSubscriptionCycleCloseCurrent(orgSubscription.id))) { + logger.debug('Previous period cycle close pending; deferring org threshold billing', { + organizationId, + }) + return noOp(options, 'pending-cycle-close') + } + logger.debug('Found organization subscription', { organizationId, plan: orgSubscription.plan, @@ -569,12 +582,8 @@ async function checkAndBillOrganizationOverageThreshold( .select({ userId: member.userId, role: member.role, - currentPeriodCost: userStats.currentPeriodCost, - departedMemberUsage: organization.departedMemberUsage, }) .from(member) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .innerJoin(organization, eq(organization.id, member.organizationId)) .where(eq(member.organizationId, organizationId)) logger.debug('Found organization members', { @@ -623,16 +632,12 @@ async function checkAndBillOrganizationOverageThreshold( periodStart: orgSubscription.periodStart ?? null, periodEnd: orgSubscription.periodEnd ?? null, organizationId, - pooledCurrentPeriodCost: usageSnapshot.pooledCurrentPeriodCost + ledgerUsage, - departedMemberUsage: usageSnapshot.departedMemberUsage, - memberIds: usageSnapshot.memberIds, + pooledLedgerUsage: ledgerUsage, }) if (currentOverage < threshold) { logger.debug('Organization threshold billing check below threshold before locking', { organizationId, - totalTeamUsage: - usageSnapshot.pooledCurrentPeriodCost + ledgerUsage + usageSnapshot.departedMemberUsage, ledgerUsage, effectiveTeamUsage, basePrice, @@ -702,6 +707,22 @@ async function checkAndBillOrganizationOverageThreshold( return requireSettlementState(options, 'Owner stats are required for settlement') } + // Same under-lock revalidation as the personal path (see above). + if ( + !(await isSubscriptionCycleCloseCurrent(orgSubscription.id, { + executor: tx, + expectedPeriodStart: orgSubscription.periodStart, + })) + ) { + logger.debug('Organization period advanced during threshold settlement; retry later', { + organizationId, + }) + return retryConcurrentSettlement( + options, + 'Organization period advanced during threshold settlement' + ) + } + const orgLock = await tx .select() .from(organization) @@ -718,12 +739,8 @@ async function checkAndBillOrganizationOverageThreshold( .select({ userId: member.userId, role: member.role, - currentPeriodCost: userStats.currentPeriodCost, - departedMemberUsage: organization.departedMemberUsage, }) .from(member) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .innerJoin(organization, eq(organization.id, member.organizationId)) .where(eq(member.organizationId, organizationId)) const lockedUsageSnapshot = buildOrganizationUsageSnapshot(lockedMemberUsageRows) @@ -732,15 +749,18 @@ async function checkAndBillOrganizationOverageThreshold( lockedOwnerId !== usageSnapshot.ownerId || !organizationUsageSnapshotMatches(usageSnapshot, lockedUsageSnapshot) ) { - logger.debug('Organization usage changed during threshold billing check; retry later', { - organizationId, - usageSnapshot, - lockedUsageSnapshot, - lockedOwnerId, - }) + logger.debug( + 'Organization membership changed during threshold billing check; retry later', + { + organizationId, + usageSnapshot, + lockedUsageSnapshot, + lockedOwnerId, + } + ) return retryConcurrentSettlement( options, - 'Organization usage changed during threshold settlement' + 'Organization membership changed during threshold settlement' ) } @@ -751,8 +771,6 @@ async function checkAndBillOrganizationOverageThreshold( logger.debug('Organization threshold billing check', { organizationId, - totalTeamUsage: - usageSnapshot.pooledCurrentPeriodCost + ledgerUsage + usageSnapshot.departedMemberUsage, ledgerUsage, effectiveTeamUsage, basePrice, @@ -911,77 +929,20 @@ async function checkAndBillOrganizationOverageThreshold( } } -async function getPersonalUsageSnapshot(userId: string): Promise { - const [stats] = await db - .select({ - currentPeriodCost: userStats.currentPeriodCost, - proPeriodCostSnapshot: userStats.proPeriodCostSnapshot, - proPeriodCostSnapshotAt: userStats.proPeriodCostSnapshotAt, - lastPeriodCost: userStats.lastPeriodCost, - }) - .from(userStats) - .where(eq(userStats.userId, userId)) - .limit(1) - - return stats ? personalUsageSnapshotFromStats(stats) : null -} - -function personalUsageSnapshotFromStats(stats: { - currentPeriodCost: string | number | null - proPeriodCostSnapshot: string | number | null - proPeriodCostSnapshotAt: Date | null - lastPeriodCost: string | number | null -}): PersonalUsageSnapshot { - return { - currentPeriodCost: toNumber(toDecimal(stats.currentPeriodCost)), - proPeriodCostSnapshot: toNumber(toDecimal(stats.proPeriodCostSnapshot)), - proPeriodCostSnapshotAt: stats.proPeriodCostSnapshotAt, - lastPeriodCost: toNumber(toDecimal(stats.lastPeriodCost)), - } -} - -function personalUsageSnapshotMatches( - expected: PersonalUsageSnapshot, - actual: PersonalUsageSnapshot -): boolean { - return ( - Math.abs(expected.currentPeriodCost - actual.currentPeriodCost) <= USAGE_TOTAL_EPSILON && - Math.abs(expected.proPeriodCostSnapshot - actual.proPeriodCostSnapshot) <= - USAGE_TOTAL_EPSILON && - Math.abs(expected.lastPeriodCost - actual.lastPeriodCost) <= USAGE_TOTAL_EPSILON && - nullableDateTime(expected.proPeriodCostSnapshotAt) === - nullableDateTime(actual.proPeriodCostSnapshotAt) - ) -} - function buildOrganizationUsageSnapshot( rows: { userId: string role: string - currentPeriodCost: string | number | null - departedMemberUsage: string | number | null }[] ): OrganizationUsageSnapshot | null { const owner = rows.find((row) => row.role === 'owner') if (!owner) return null const sortedRows = [...rows].sort((a, b) => a.userId.localeCompare(b.userId)) - let pooledCurrentPeriodCost = 0 - for (const row of sortedRows) { - pooledCurrentPeriodCost += toNumber(toDecimal(row.currentPeriodCost)) - } return { - memberIds: sortedRows.map((row) => row.userId), ownerId: owner.userId, - memberSignature: sortedRows - .map( - (row) => - `${row.userId}:${row.role}:${toNumber(toDecimal(row.currentPeriodCost)).toFixed(6)}` - ) - .join('|'), - pooledCurrentPeriodCost, - departedMemberUsage: toNumber(toDecimal(owner.departedMemberUsage)), + memberSignature: sortedRows.map((row) => `${row.userId}:${row.role}`).join('|'), } } @@ -989,13 +950,5 @@ function organizationUsageSnapshotMatches( expected: OrganizationUsageSnapshot, actual: OrganizationUsageSnapshot ): boolean { - return ( - expected.ownerId === actual.ownerId && - expected.memberSignature === actual.memberSignature && - Math.abs(expected.departedMemberUsage - actual.departedMemberUsage) <= USAGE_TOTAL_EPSILON - ) -} - -function nullableDateTime(value: Date | null): number | null { - return value?.getTime() ?? null + return expected.ownerId === actual.ownerId && expected.memberSignature === actual.memberSignature } diff --git a/apps/sim/lib/billing/webhooks/invoices.test.ts b/apps/sim/lib/billing/webhooks/invoices.test.ts index 2a117da98a1..cdf4d961e77 100644 --- a/apps/sim/lib/billing/webhooks/invoices.test.ts +++ b/apps/sim/lib/billing/webhooks/invoices.test.ts @@ -96,7 +96,6 @@ vi.mock('@/lib/messaging/email/validation', () => ({ import { handleInvoicePaymentFailed, handleInvoicePaymentSucceeded, - resetUsageForSubscription, } from '@/lib/billing/webhooks/invoices' import { sendEmail } from '@/lib/messaging/email/mailer' @@ -263,51 +262,4 @@ describe('invoice billing recovery', () => { expect(mockUnblockOrgMembers).toHaveBeenCalledWith('org-1', 'payment_failed') expect(mockBlockOrgMembers).not.toHaveBeenCalled() }) - - it('locks member userStats before the organization row during usage reset', async () => { - queueSelectResponse({ limitResult: [{ userId: 'owner-1' }] }) // owner member row - queueSelectResponse({ limitResult: [{ userId: 'owner-1' }] }) // owner userStats - queueSelectResponse({ whereResult: [{ userId: 'owner-1' }, { userId: 'member-1' }] }) // member ids - queueSelectResponse({ whereResult: [] }) // all-member userStats FOR UPDATE (pre-org lock) - queueSelectResponse({ limitResult: [{ id: 'org-1' }] }) // organization - queueSelectResponse({ - whereResult: [ - { userId: 'owner-1', current: '125', currentCopilot: '10' }, - { userId: 'member-1', current: '75', currentCopilot: '5' }, - ], - }) - queueSelectResponse({ whereResult: [] }) - queueSelectResponse({ whereResult: [] }) - - await resetUsageForSubscription({ plan: 'team', referenceId: 'org-1' }) - - expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(1) - expect(dbChainMockFns.update).toHaveBeenCalledTimes(2) - - const whereArgs = dbChainMockFns.where.mock.calls.map( - (call) => call[0] as { type?: string; column?: string; left?: string } - ) - const allMemberStatsLockIndex = whereArgs.findIndex( - (arg) => arg?.type === 'inArray' && arg?.column === 'userStats.userId' - ) - const orgLockIndex = whereArgs.findIndex( - (arg) => arg?.type === 'eq' && arg?.left === 'organization.id' - ) - expect(allMemberStatsLockIndex).toBeGreaterThanOrEqual(0) - expect(orgLockIndex).toBeGreaterThanOrEqual(0) - expect(allMemberStatsLockIndex).toBeLessThan(orgLockIndex) - - const statsReset = dbChainMockFns.set.mock.calls[0][0] as Record - expect(statsReset.currentPeriodCost).not.toBe('0') - expect(statsReset.currentPeriodCopilotCost).not.toBe('0') - expect(statsReset.lastPeriodCost).toMatchObject({ - toSQL: expect.any(Function), - }) - expect((statsReset.lastPeriodCost as { toSQL: () => { sql: string } }).toSQL().sql).toContain( - 'CASE' - ) - expect( - (statsReset.currentPeriodCost as { toSQL: () => { sql: string } }).toSQL().sql - ).toContain('GREATEST') - }) }) diff --git a/apps/sim/lib/billing/webhooks/invoices.ts b/apps/sim/lib/billing/webhooks/invoices.ts index a89383ccab0..6411c654d10 100644 --- a/apps/sim/lib/billing/webhooks/invoices.ts +++ b/apps/sim/lib/billing/webhooks/invoices.ts @@ -1,33 +1,20 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' -import { - member, - organization, - subscription as subscriptionTable, - user, - userStats, -} from '@sim/db/schema' +import { member, subscription as subscriptionTable, user, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { isOrgAdminRole } from '@sim/platform-authz/workspace' -import { and, eq, inArray, isNull, ne, or, sql } from 'drizzle-orm' +import { and, eq, inArray, isNull, ne, or } from 'drizzle-orm' import type Stripe from 'stripe' import { getEmailSubject, renderCreditPurchaseEmail, renderPaymentFailedEmail, } from '@/components/emails' -import { BILLING_LOCK_TIMEOUT_MS } from '@/lib/billing/constants' -import { calculateSubscriptionOverage, isSubscriptionOrgScoped } from '@/lib/billing/core/billing' -import { - COPILOT_USAGE_SOURCES, - getBillingPeriodUsageCostByUser, -} from '@/lib/billing/core/usage-log' +import { isSubscriptionOrgScoped } from '@/lib/billing/core/billing' import { addCredits, getCreditBalanceForEntity } from '@/lib/billing/credits/balance' import { setUsageLimitForCredits } from '@/lib/billing/credits/purchase' import { blockOrgMembers, unblockOrgMembers } from '@/lib/billing/organizations/membership' -import { isEnterprise } from '@/lib/billing/plan-helpers' import { requireStripeClient } from '@/lib/billing/stripe-client' -import { resolveDefaultPaymentMethod } from '@/lib/billing/stripe-payment-method' import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { toDecimal, toNumber } from '@/lib/billing/utils/decimal' import { stripeWebhookIdempotency } from '@/lib/billing/webhooks/idempotency' @@ -39,6 +26,17 @@ import { captureServerEvent } from '@/lib/posthog/server' const logger = createLogger('StripeInvoiceWebhooks') +/** + * Cycle rollover (usage window advance, final overage collection, + * `billedOverageThisPeriod` reset, last-period bookkeeping) is NOT handled + * here. Usage windows advance automatically — current usage is the attributed + * usage_log ledger for the subscription's current period — and the money + + * bookkeeping close runs off period advance in + * `@/lib/billing/cycle-close`, independent of invoice payload shape. These + * handlers only manage payment lifecycle: block/unblock, notification emails, + * credit purchases, and audit. + */ + /** * Resolve the audit actor for a billing event. For org-scoped subscriptions the * actor is the org owner; for personal subscriptions it is the reference (user) @@ -64,26 +62,6 @@ async function resolveBillingActorId(isOrgScoped: boolean, referenceId: string): } } -function getSubscriptionLinePeriod( - invoice: Stripe.Invoice, - stripeSubscriptionId: string -): { periodStart: Date; periodEnd: Date } | null { - const subscriptionLine = invoice.lines?.data?.find( - (line) => - line.parent?.type === 'subscription_item_details' && - line.parent.subscription_item_details?.subscription === stripeSubscriptionId - ) - - if (!subscriptionLine?.period?.start || !subscriptionLine.period.end) { - return null - } - - return { - periodStart: new Date(subscriptionLine.period.start * 1000), - periodEnd: new Date(subscriptionLine.period.end * 1000), - } -} - const METADATA_SUBSCRIPTION_INVOICE_TYPES = new Set([ 'overage_billing', 'overage_threshold_billing', @@ -438,224 +416,6 @@ export async function getBilledOverageForSubscription(sub: { : 0 } -export async function resetUsageForSubscription(sub: { - plan: string | null - referenceId: string - periodStart?: Date | null - periodEnd?: Date | null -}) { - const billingPeriod = - sub.periodStart && sub.periodEnd ? { start: sub.periodStart, end: sub.periodEnd } : null - - if (await isSubscriptionOrgScoped(sub)) { - const ledgerUsageByUser = billingPeriod - ? await getBillingPeriodUsageCostByUser( - { type: 'organization', id: sub.referenceId }, - billingPeriod - ) - : new Map() - // Copilot-family ledger per user, so last-period copilot mirrors last-period - // cost (baseline + usage_log) instead of capturing the baseline alone. - const copilotLedgerByUser = billingPeriod - ? await getBillingPeriodUsageCostByUser( - { type: 'organization', id: sub.referenceId }, - billingPeriod, - COPILOT_USAGE_SOURCES - ) - : new Map() - - await db.transaction(async (tx) => { - await tx.execute(sql.raw(`SET LOCAL lock_timeout = '${BILLING_LOCK_TIMEOUT_MS}ms'`)) - - const ownerRows = await tx - .select({ userId: member.userId }) - .from(member) - .where(and(eq(member.organizationId, sub.referenceId), eq(member.role, 'owner'))) - .for('update') - .limit(1) - - const ownerId = ownerRows[0]?.userId - if (ownerId) { - await tx - .select({ userId: userStats.userId }) - .from(userStats) - .where(eq(userStats.userId, ownerId)) - .for('update') - .limit(1) - } - - const membersRows = await tx - .select({ userId: member.userId }) - .from(member) - .where(eq(member.organizationId, sub.referenceId)) - - const memberIds = membersRows.map((row) => row.userId) - - // Lock every member's userStats before the organization row so this path - // follows the canonical userStats → organization order shared by the - // join, remove, threshold-billing, and storage-transfer paths. Locking - // organization first would invert against them and risk an AB-BA - // deadlock. The per-member UPDATE below re-locks these rows (no-op). - if (memberIds.length > 0) { - await tx - .select({ userId: userStats.userId }) - .from(userStats) - .where(inArray(userStats.userId, memberIds)) - .for('update') - } - - await tx - .select({ id: organization.id }) - .from(organization) - .where(eq(organization.id, sub.referenceId)) - .for('update') - .limit(1) - if (memberIds.length > 0) { - const memberStatsRows = await tx - .select({ - userId: userStats.userId, - current: userStats.currentPeriodCost, - currentCopilot: userStats.currentPeriodCopilotCost, - }) - .from(userStats) - .where(inArray(userStats.userId, memberIds)) - - const statsUserIds = memberStatsRows.map((row) => row.userId) - if (statsUserIds.length === 0) { - await tx - .update(organization) - .set({ departedMemberUsage: '0' }) - .where(eq(organization.id, sub.referenceId)) - return - } - - const lastCostByUser = sql.join( - memberStatsRows.map((row) => { - const baseline = toNumber(toDecimal(row.current)) - const ledgerUsage = ledgerUsageByUser.get(row.userId) ?? 0 - const lastPeriodCost = (baseline + ledgerUsage).toString() - return sql`WHEN ${row.userId} THEN ${lastPeriodCost}` - }), - sql` ` - ) - const currentCostByUser = sql.join( - memberStatsRows.map((row) => sql`WHEN ${row.userId} THEN ${row.current ?? '0'}`), - sql` ` - ) - const currentCopilotCostByUser = sql.join( - memberStatsRows.map((row) => sql`WHEN ${row.userId} THEN ${row.currentCopilot ?? '0'}`), - sql` ` - ) - // Last-period copilot = baseline copilot + copilot-family ledger, mirroring - // lastPeriodCost. (The reset below still subtracts only the baseline, since - // the ledger is period-scoped and rolls over on its own.) - const lastCopilotCostByUser = sql.join( - memberStatsRows.map((row) => { - const baselineCopilot = toNumber(toDecimal(row.currentCopilot)) - const copilotLedger = copilotLedgerByUser.get(row.userId) ?? 0 - return sql`WHEN ${row.userId} THEN ${(baselineCopilot + copilotLedger).toString()}` - }), - sql` ` - ) - const capturedLastCost = sql`CASE ${userStats.userId} ${lastCostByUser} ELSE '0' END` - const capturedCurrentCost = sql`CASE ${userStats.userId} ${currentCostByUser} ELSE '0' END` - const capturedCurrentCopilotCost = sql`CASE ${userStats.userId} ${currentCopilotCostByUser} ELSE '0' END` - const capturedLastCopilotCost = sql`CASE ${userStats.userId} ${lastCopilotCostByUser} ELSE '0' END` - - await tx - .update(userStats) - .set({ - lastPeriodCost: capturedLastCost, - lastPeriodCopilotCost: capturedLastCopilotCost, - currentPeriodCost: sql`GREATEST(0, ${userStats.currentPeriodCost} - (${capturedCurrentCost})::decimal)`, - currentPeriodCopilotCost: sql`GREATEST(0, ${userStats.currentPeriodCopilotCost} - (${capturedCurrentCopilotCost})::decimal)`, - billedOverageThisPeriod: '0', - }) - .where(inArray(userStats.userId, statsUserIds)) - } - - await tx - .update(organization) - .set({ departedMemberUsage: '0' }) - .where(eq(organization.id, sub.referenceId)) - }) - } else { - const currentStats = await db - .select({ - current: userStats.currentPeriodCost, - snapshot: userStats.proPeriodCostSnapshot, - currentCopilot: userStats.currentPeriodCopilotCost, - }) - .from(userStats) - .where(eq(userStats.userId, sub.referenceId)) - .limit(1) - if (currentStats.length > 0) { - const current = currentStats[0].current || '0' - const snapshot = toNumber(toDecimal(currentStats[0].snapshot)) - const currentCopilot = currentStats[0].currentCopilot || '0' - const ledgerUsage = billingPeriod - ? await getBillingPeriodUsageCostByUser( - { type: 'user', id: sub.referenceId }, - billingPeriod - ) - : new Map() - const userLedgerUsage = ledgerUsage.get(sub.referenceId) ?? 0 - const copilotLedgerUsage = billingPeriod - ? (( - await getBillingPeriodUsageCostByUser( - { type: 'user', id: sub.referenceId }, - billingPeriod, - COPILOT_USAGE_SOURCES - ) - ).get(sub.referenceId) ?? 0) - : 0 - - // Snapshot > 0: user joined a paid org mid-cycle. The pre-join - // portion was billed on this invoice (snapshot); `currentPeriodCost` - // is post-join usage the org will bill next cycle-close, so keep - // it. Only retire the personal-billing trackers here. - if (snapshot > 0) { - await db - .update(userStats) - .set({ - lastPeriodCost: (snapshot + userLedgerUsage).toString(), - // Pre-join personal copilot = the user-scoped copilot ledger only - // (post-join copilot usage is org-attributed, so this captures the - // pre-join portion). The copilot baseline stays with the org via the - // retained currentPeriodCopilotCost, so don't add it here (avoids a - // double count at the org's cycle-close). - lastPeriodCopilotCost: copilotLedgerUsage.toString(), - proPeriodCostSnapshot: '0', - proPeriodCostSnapshotAt: null, - billedOverageThisPeriod: '0', - }) - .where(eq(userStats.userId, sub.referenceId)) - } else { - const totalLastPeriod = ( - toNumber(toDecimal(current)) + - snapshot + - userLedgerUsage - ).toString() - // Delta-reset for the same reason as the org branch above. - await db - .update(userStats) - .set({ - lastPeriodCost: totalLastPeriod, - lastPeriodCopilotCost: ( - toNumber(toDecimal(currentCopilot)) + copilotLedgerUsage - ).toString(), - currentPeriodCost: sql`GREATEST(0, ${userStats.currentPeriodCost} - ${current}::decimal)`, - currentPeriodCopilotCost: sql`GREATEST(0, ${userStats.currentPeriodCopilotCost} - ${currentCopilot}::decimal)`, - proPeriodCostSnapshot: '0', - proPeriodCostSnapshotAt: null, - billedOverageThisPeriod: '0', - }) - .where(eq(userStats.userId, sub.referenceId)) - } - } - } -} - /** * Handle credit purchase invoice payment succeeded. */ @@ -854,30 +614,6 @@ export async function handleInvoicePaymentSucceeded(event: Stripe.Event) { const { sub } = resolvedInvoice const subIsOrgScoped = await isSubscriptionOrgScoped(sub) - let wasBlocked = false - if (subIsOrgScoped) { - const membersRows = await db - .select({ userId: member.userId }) - .from(member) - .where(eq(member.organizationId, sub.referenceId)) - const memberIds = membersRows.map((m) => m.userId) - if (memberIds.length > 0) { - const blockedRows = await db - .select({ blocked: userStats.billingBlocked }) - .from(userStats) - .where(inArray(userStats.userId, memberIds)) - - wasBlocked = blockedRows.some((row) => !!row.blocked) - } - } else { - const row = await db - .select({ blocked: userStats.billingBlocked }) - .from(userStats) - .where(eq(userStats.userId, sub.referenceId)) - .limit(1) - wasBlocked = row.length > 0 ? !!row[0].blocked : false - } - const isProrationInvoice = invoice.billing_reason === 'subscription_update' const shouldUnblock = !isProrationInvoice || (invoice.amount_paid ?? 0) > 0 @@ -903,19 +639,6 @@ export async function handleInvoicePaymentSucceeded(event: Stripe.Event) { }) } - if (wasBlocked && !isProrationInvoice) { - const invoicePeriod = getSubscriptionLinePeriod( - invoice, - resolvedInvoice.stripeSubscriptionId - ) - await resetUsageForSubscription({ - plan: sub.plan, - referenceId: sub.referenceId, - periodStart: invoicePeriod?.periodStart ?? null, - periodEnd: invoicePeriod?.periodEnd ?? null, - }) - } - const entityType = subIsOrgScoped ? 'organization' : 'user' const amountPaid = (invoice.amount_paid ?? 0) / 100 const actorId = await resolveBillingActorId(subIsOrgScoped, sub.referenceId) @@ -1092,268 +815,3 @@ export async function handleInvoicePaymentFailed(event: Stripe.Event) { throw error } } - -/** - * Handle base invoice finalized → create a separate overage-only invoice - * Note: Enterprise plans no longer have overages - */ -export async function handleInvoiceFinalized(event: Stripe.Event) { - try { - const invoice = event.data.object as Stripe.Invoice - const subscription = invoice.parent?.subscription_details?.subscription - const stripeSubscriptionId = typeof subscription === 'string' ? subscription : subscription?.id - if (!stripeSubscriptionId) { - logger.info('No subscription found on invoice; skipping finalized handler', { - invoiceId: invoice.id, - }) - return - } - if (invoice.billing_reason && invoice.billing_reason !== 'subscription_cycle') return - - const records = await db - .select() - .from(subscriptionTable) - .where(eq(subscriptionTable.stripeSubscriptionId, stripeSubscriptionId)) - .limit(1) - - if (records.length === 0) return - const sub = records[0] - - const invoicePeriod = getSubscriptionLinePeriod(invoice, stripeSubscriptionId) - if (!invoicePeriod) { - logger.error('Missing subscription line period on subscription cycle invoice', { - invoiceId: invoice.id, - stripeSubscriptionId, - }) - if (isEnterprise(sub.plan)) { - await resetUsageForSubscription({ plan: sub.plan, referenceId: sub.referenceId }) - } - return - } - - if (isEnterprise(sub.plan)) { - await resetUsageForSubscription({ - plan: sub.plan, - referenceId: sub.referenceId, - periodStart: invoicePeriod.periodStart, - periodEnd: invoicePeriod.periodEnd, - }) - return - } - - await stripeWebhookIdempotency.executeWithIdempotency( - 'invoice-finalized', - event.id, - async () => { - const stripe = requireStripeClient() - const periodStart = Math.floor(invoicePeriod.periodStart.getTime() / 1000) - const periodEnd = Math.floor(invoicePeriod.periodEnd.getTime() / 1000) - const billingPeriod = new Date(periodEnd * 1000).toISOString().slice(0, 7) - - const totalOverage = await calculateSubscriptionOverage({ - ...sub, - periodStart: new Date(periodStart * 1000), - periodEnd: new Date(periodEnd * 1000), - }) - - const entityType = (await isSubscriptionOrgScoped(sub)) ? 'organization' : 'user' - const entityId = sub.referenceId - - // Phase 1 — atomic commit. Resolve org owners inside the transaction, - // then lock the tracker row so `billedOverageThisPeriod` is serialized - // against threshold billing, resets, owner transfers, and retries. - const phase1 = await db.transaction(async (tx) => { - await tx.execute(sql.raw(`SET LOCAL lock_timeout = '${BILLING_LOCK_TIMEOUT_MS}ms'`)) - - let trackerUserId = entityId - if (entityType === 'organization') { - const ownerRows = await tx - .select({ userId: member.userId }) - .from(member) - .where(and(eq(member.organizationId, entityId), eq(member.role, 'owner'))) - .for('update') - .limit(1) - const ownerId = ownerRows[0]?.userId - if (!ownerId) { - throw new Error( - `Organization ${entityId} has no owner member; cannot process invoice finalization` - ) - } - trackerUserId = ownerId - } - - const trackerRows = await tx - .select({ billed: userStats.billedOverageThisPeriod }) - .from(userStats) - .where(eq(userStats.userId, trackerUserId)) - .for('update') - .limit(1) - - const billedInTx = trackerRows.length > 0 ? toNumber(toDecimal(trackerRows[0].billed)) : 0 - const remaining = Math.max(0, totalOverage - billedInTx) - - if (remaining === 0) { - return { billedInTx, applied: 0, billed: 0, remaining: 0 } - } - - const lockedBalance = - entityType === 'organization' - ? await tx - .select({ creditBalance: organization.creditBalance }) - .from(organization) - .where(eq(organization.id, entityId)) - .for('update') - .limit(1) - : await tx - .select({ creditBalance: userStats.creditBalance }) - .from(userStats) - .where(eq(userStats.userId, entityId)) - .for('update') - .limit(1) - - const creditBalance = - lockedBalance.length > 0 ? toNumber(toDecimal(lockedBalance[0].creditBalance)) : 0 - - const applied = Math.min(creditBalance, remaining) - const billed = remaining - applied - - if (applied > 0) { - if (entityType === 'organization') { - await tx - .update(organization) - .set({ - creditBalance: sql`GREATEST(0, ${organization.creditBalance} - ${applied})`, - }) - .where(eq(organization.id, entityId)) - } else { - await tx - .update(userStats) - .set({ - creditBalance: sql`GREATEST(0, ${userStats.creditBalance} - ${applied})`, - }) - .where(eq(userStats.userId, entityId)) - } - } - - await tx - .update(userStats) - .set({ billedOverageThisPeriod: totalOverage.toString() }) - .where(eq(userStats.userId, trackerUserId)) - - return { billedInTx, applied, billed, remaining } - }) - - const creditsApplied = phase1.applied - const amountToBillStripe = phase1.billed - - logger.info('Invoice finalized overage calculation', { - subscriptionId: sub.id, - totalOverage, - billedOverageBeforeTx: phase1.billedInTx, - creditsApplied, - amountToBillStripe, - billingPeriod, - }) - - // Phase 2 — Stripe invoice. Runs outside any DB transaction. - // Every call uses a deterministic idempotency key so retries - // converge on the same invoice object: re-create returns the - // existing draft, re-finalize no-ops on an already-finalized - // invoice, re-pay no-ops on an already-paid invoice. - if (amountToBillStripe > 0) { - const customerId = String(invoice.customer) - const cents = Math.round(amountToBillStripe * 100) - const itemIdemKey = `overage-item:${customerId}:${stripeSubscriptionId}:${billingPeriod}` - const invoiceIdemKey = `overage-invoice:${customerId}:${stripeSubscriptionId}:${billingPeriod}` - const finalizeIdemKey = `overage-finalize:${customerId}:${stripeSubscriptionId}:${billingPeriod}` - const payIdemKey = `overage-pay:${customerId}:${stripeSubscriptionId}:${billingPeriod}` - - const { paymentMethodId: defaultPaymentMethod, collectionMethod } = - await resolveDefaultPaymentMethod(stripe, stripeSubscriptionId, customerId) - - const effectiveCollectionMethod = collectionMethod ?? 'charge_automatically' - - const overageInvoice = await stripe.invoices.create( - { - customer: customerId, - collection_method: effectiveCollectionMethod, - auto_advance: false, - ...(defaultPaymentMethod ? { default_payment_method: defaultPaymentMethod } : {}), - metadata: { - type: 'overage_billing', - billingPeriod, - subscriptionId: stripeSubscriptionId, - }, - }, - { idempotencyKey: invoiceIdemKey } - ) - - await stripe.invoiceItems.create( - { - customer: customerId, - invoice: overageInvoice.id, - amount: cents, - currency: 'usd', - description: `Usage Based Overage – ${billingPeriod}`, - metadata: { - type: 'overage_billing', - billingPeriod, - subscriptionId: stripeSubscriptionId, - }, - }, - { idempotencyKey: itemIdemKey } - ) - - const draftId = overageInvoice.id - if (typeof draftId !== 'string' || draftId.length === 0) { - logger.error('Stripe created overage invoice without id; aborting finalize') - } else { - const finalized = await stripe.invoices.finalizeInvoice( - draftId, - {}, - { idempotencyKey: finalizeIdemKey } - ) - if ( - effectiveCollectionMethod === 'charge_automatically' && - finalized.status === 'open' - ) { - try { - const payId = finalized.id - if (typeof payId !== 'string' || payId.length === 0) { - logger.error('Finalized invoice missing id') - throw new Error('Finalized invoice missing id') - } - await stripe.invoices.pay( - payId, - { payment_method: defaultPaymentMethod }, - { idempotencyKey: payIdemKey } - ) - } catch (payError) { - logger.error('Failed to auto-pay overage invoice', { - error: payError, - invoiceId: finalized.id, - }) - } - } - } - } - - // Phase 3 — reset usage for the new period. Clears trackers and - // rolls `currentPeriodCost` forward by delta. Idempotent on its - // own (delta subtraction of a value that's already been - // subtracted is a no-op). - await resetUsageForSubscription({ - plan: sub.plan, - referenceId: sub.referenceId, - periodStart: invoicePeriod.periodStart, - periodEnd: invoicePeriod.periodEnd, - }) - - return { totalOverage, creditsApplied, amountToBillStripe } - } - ) - } catch (error) { - logger.error('Failed to handle invoice finalized', { error }) - throw error - } -} diff --git a/apps/sim/lib/billing/webhooks/subscription.ts b/apps/sim/lib/billing/webhooks/subscription.ts index 6ce6c72f5de..f6528ca3906 100644 --- a/apps/sim/lib/billing/webhooks/subscription.ts +++ b/apps/sim/lib/billing/webhooks/subscription.ts @@ -5,15 +5,17 @@ import { createLogger } from '@sim/logger' import { and, eq, inArray, ne } from 'drizzle-orm' import { calculateSubscriptionOverage, isSubscriptionOrgScoped } from '@/lib/billing/core/billing' import { syncUsageLimitsFromSubscription } from '@/lib/billing/core/usage' +import { + claimTerminalPeriod, + closeElapsedPeriodBeforeDeletion, + writeFinalPeriodBookkeeping, +} from '@/lib/billing/cycle-close' import { restoreUserProSubscription } from '@/lib/billing/organizations/membership' import { isEnterprise, isPaid, isPro, isTeam } from '@/lib/billing/plan-helpers' import { requireStripeClient } from '@/lib/billing/stripe-client' import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { stripeWebhookIdempotency } from '@/lib/billing/webhooks/idempotency' -import { - getBilledOverageForSubscription, - resetUsageForSubscription, -} from '@/lib/billing/webhooks/invoices' +import { getBilledOverageForSubscription } from '@/lib/billing/webhooks/invoices' import { captureServerEvent } from '@/lib/posthog/server' import { detachOrganizationWorkspaces } from '@/lib/workspaces/organization-workspaces' @@ -185,35 +187,17 @@ export async function handleSubscriptionCreated( const wasFreePreviously = otherActiveSubscriptions.length === 0 const isPaidPlan = isPaid(subscriptionData.plan) - if (wasFreePreviously && isPaidPlan) { - logger.info('Detected free -> paid transition, resetting usage', { - subscriptionId: subscriptionData.id, - referenceId: subscriptionData.referenceId, - plan: subscriptionData.plan, - }) - - await resetUsageForSubscription({ - plan: subscriptionData.plan, - referenceId: subscriptionData.referenceId, - periodStart: subscriptionData.periodStart ?? null, - periodEnd: subscriptionData.periodEnd ?? null, - }) - - logger.info('Successfully reset usage for free -> paid transition', { - subscriptionId: subscriptionData.id, - referenceId: subscriptionData.referenceId, - plan: subscriptionData.plan, - }) - } else { - logger.info('No usage reset needed', { - subscriptionId: subscriptionData.id, - referenceId: subscriptionData.referenceId, - plan: subscriptionData.plan, - wasFreePreviously, - isPaidPlan, - otherActiveSubscriptionsCount: otherActiveSubscriptions.length, - }) - } + // No usage reset on free -> paid: usage is the attributed ledger, and + // the new subscription's period window starts empty by construction + // (rows are stamped with the paid period at write time). + logger.info('Processed subscription creation', { + subscriptionId: subscriptionData.id, + referenceId: subscriptionData.referenceId, + plan: subscriptionData.plan, + wasFreePreviously, + isPaidPlan, + otherActiveSubscriptionsCount: otherActiveSubscriptions.length, + }) if (wasFreePreviously && isPaidPlan) { // Best-effort instrumentation; a transient DB error here must never abort @@ -276,8 +260,10 @@ export async function handleSubscriptionDeleted( referenceId: string stripeSubscriptionId: string | null seats?: number | null + billingInterval?: string | null periodStart?: Date | null periodEnd?: Date | null + metadata?: unknown }, stripeEventId?: string ) { @@ -299,15 +285,45 @@ export async function handleSubscriptionDeleted( 'subscription-deleted', idempotencyIdentifier, async () => { - const totalOverage = await calculateSubscriptionOverage(subscription) + // Settle any elapsed period the sweep has not closed yet — a deleted + // subscription leaves the sweep's candidate set, so this is the last + // chance to bill it (and to reset the threshold tracker so the + // terminal settlement below is not offset by the elapsed period's + // collections). + await closeElapsedPeriodBeforeDeletion(subscription.id) + + // Then claim the terminal period BEFORE computing or charging: this + // reads the row's fresh period (webhook payloads can be stale across + // a rollover) and serializes with the cycle-close sweep. A lagging + // marker here means the close above deferred OR a rollover committed + // in between — run the close once more (it settles a freshly elapsed + // period; a deferred close defers again, loudly), then seal so the + // marker cannot be raced indefinitely and an in-flight sweep aborts + // its conflicting close. + let terminal = await claimTerminalPeriod(subscription.id) + if (!terminal.markerWasCurrent) { + await closeElapsedPeriodBeforeDeletion(subscription.id) + terminal = await claimTerminalPeriod(subscription.id, { sealLagging: true }) + } + const settlementPeriod = { + periodStart: terminal.periodStart ?? subscription.periodStart ?? null, + periodEnd: terminal.periodEnd ?? subscription.periodEnd ?? null, + } + + const totalOverage = await calculateSubscriptionOverage({ + ...subscription, + ...settlementPeriod, + }) const stripe = requireStripeClient() if (isEnterprise(subscription.plan)) { - await resetUsageForSubscription({ + await writeFinalPeriodBookkeeping({ + id: subscription.id, plan: subscription.plan, referenceId: subscription.referenceId, - periodStart: subscription.periodStart ?? null, - periodEnd: subscription.periodEnd ?? null, + billingInterval: subscription.billingInterval ?? null, + ...settlementPeriod, + metadata: subscription.metadata, }) const dormantResult = await transitionOrganizationToDormantState( @@ -343,7 +359,15 @@ export async function handleSubscriptionDeleted( return { totalOverage: 0, kind: 'enterprise' as const } } - const billedOverage = await getBilledOverageForSubscription(subscription) + // The tracker only ever holds collections for the period that began + // at the close marker — the threshold gate blocks settlement while + // the marker lags. If the marker was still lagging at claim time + // (the elapsed close above deferred), the tracked amount belongs to + // that forgiven elapsed period, not the terminal window: subtracting + // it would under-bill the final invoice, so count nothing. + const billedOverage = terminal.markerWasCurrent + ? await getBilledOverageForSubscription(subscription) + : 0 const remainingOverage = Math.max(0, totalOverage - billedOverage) logger.info('Subscription deleted overage calculation', { @@ -423,11 +447,13 @@ export async function handleSubscriptionDeleted( }) } - await resetUsageForSubscription({ + await writeFinalPeriodBookkeeping({ + id: subscription.id, plan: subscription.plan, referenceId: subscription.referenceId, - periodStart: subscription.periodStart ?? null, - periodEnd: subscription.periodEnd ?? null, + billingInterval: subscription.billingInterval ?? null, + ...settlementPeriod, + metadata: subscription.metadata, }) let restoredProCount = 0 diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index 00d04a72b2d..8b811c0b7de 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -1,9 +1,7 @@ import { db, dbFor } from '@sim/db' import { - member, organization, usageLog, - userStats, user as userTable, workflow, workflowExecutionLogs, @@ -1301,16 +1299,6 @@ export class ExecutionLogger implements IExecutionLoggerService { payerSubscription.plan, payerSubscription.seats ) - let orgBaseline = 0 - if (exactBillingContext.billingPeriod.source !== 'reporting') { - const [{ sum }] = await db - .select({ sum: sql`COALESCE(SUM(${userStats.currentPeriodCost}), 0)` }) - .from(member) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, organizationId)) - .limit(1) - orgBaseline = Number.parseFloat(String(sum ?? '0')) - } const { getBillingPeriodUsageCost } = await import('@/lib/billing/core/usage-log') const orgLedger = await getBillingPeriodUsageCost( billingAttribution.billingEntity, @@ -1321,7 +1309,7 @@ export class ExecutionLogger implements IExecutionLoggerService { organizationId, planName: getDisplayPlanName(payerSubscription.plan), orgLimit, - orgUsageBefore: orgBaseline + orgLedger, + orgUsageBefore: orgLedger, } } else if (billingAttribution?.billingEntity.type === 'user' && usr?.email) { const sub = await getHighestPriorityPersonalSubscription(usr.id) diff --git a/docker/crontab b/docker/crontab index e12bb729a0b..f945efafcca 100644 --- a/docker/crontab +++ b/docker/crontab @@ -42,6 +42,9 @@ SHELL=/bin/sh # Microsoft Graph subscription renewal (Teams chat triggers expire after ~3 days) 0 */12 * * * curl -fsS -m 120 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/cron/renew-subscriptions" +# Billing cycle close: final overage collection + per-period tracker reset for elapsed periods +0 */6 * * * curl -fsS -m 120 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/cron/billing-cycle-close" + # Reclaims sandbox images 30 4 * * * curl -fsS -m 300 -o /dev/null -H "Authorization: Bearer $CRON_SECRET" "$SIM_URL/api/cron/cleanup-sandbox-images" diff --git a/helm/sim/Chart.yaml b/helm/sim/Chart.yaml index c60e2c89ce9..aab4d989ccf 100644 --- a/helm/sim/Chart.yaml +++ b/helm/sim/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: sim description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents type: application -version: 1.5.4 +version: 1.6.0 appVersion: "v0.7.44" kubeVersion: ">=1.25.0-0" home: https://sim.ai diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 88a5a1f77c2..6f71948875a 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -1387,6 +1387,15 @@ cronjobs: successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 + billingCycleClose: + enabled: true + name: billing-cycle-close + schedule: "0 */6 * * *" + path: "/api/cron/billing-cycle-close" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + reconcileBillingSeats: enabled: true name: reconcile-billing-seats diff --git a/packages/db/migrations/0305_add_subscription_last_closed_period_start.sql b/packages/db/migrations/0305_add_subscription_last_closed_period_start.sql new file mode 100644 index 00000000000..47b1e8cc227 --- /dev/null +++ b/packages/db/migrations/0305_add_subscription_last_closed_period_start.sql @@ -0,0 +1 @@ +ALTER TABLE "subscription" ADD COLUMN "last_closed_period_start" timestamp; \ No newline at end of file diff --git a/packages/db/migrations/meta/0305_snapshot.json b/packages/db/migrations/meta/0305_snapshot.json new file mode 100644 index 00000000000..84a6241fe96 --- /dev/null +++ b/packages/db/migrations/meta/0305_snapshot.json @@ -0,0 +1,20120 @@ +{ + "id": "10267de3-7569-40c6-895c-800b736a161e", + "prevId": "8b1525ae-147f-4e93-a624-0f5cff95a1d5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_unreconciled_terminal_idx": { + "name": "async_jobs_schedule_unreconciled_terminal_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unredacted": { + "name": "unredacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_app_id": { + "name": "authorization_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_enrollment_id": { + "name": "credential_group_enrollment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_scope_version": { + "name": "managed_oauth_scope_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_status": { + "name": "managed_oauth_status", + "type": "managed_oauth_credential_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "encrypted_oauth_token_set": { + "name": "encrypted_oauth_token_set", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_idx": { + "name": "credential_group_enrollment_idx", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_option_unique": { + "name": "credential_group_option_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_group_option_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_oauth'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk": { + "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk", + "tableFrom": "credential", + "tableTo": "credential_group_enrollment", + "columnsFrom": ["credential_group_enrollment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_managed_oauth_source_check": { + "name": "credential_managed_oauth_source_check", + "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )" + }, + "credential_managed_oauth_group_binding_check": { + "name": "credential_managed_oauth_group_binding_check", + "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_group": { + "name": "credential_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_provider_configuration": { + "name": "encrypted_provider_configuration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_public_id_unique": { + "name": "credential_group_public_id_unique", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_status_idx": { + "name": "credential_group_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_name_unique": { + "name": "credential_group_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"name\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_workspace_id_workspace_id_fk": { + "name": "credential_group_workspace_id_workspace_id_fk", + "tableFrom": "credential_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_created_by_user_id_fk": { + "name": "credential_group_created_by_user_id_fk", + "tableFrom": "credential_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_group_enrollment": { + "name": "credential_group_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_group_enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "invitation_token_hash": { + "name": "invitation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_expires_at": { + "name": "invitation_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_delivery_error": { + "name": "last_delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_enrollment_group_email_unique": { + "name": "credential_group_enrollment_group_email_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_invitation_token_hash_unique": { + "name": "credential_group_enrollment_invitation_token_hash_unique", + "columns": [ + { + "expression": "invitation_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_status_idx": { + "name": "credential_group_enrollment_group_status_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_invited_at_id_idx": { + "name": "credential_group_enrollment_group_invited_at_id_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invited_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_enrollment_credential_group_id_credential_group_id_fk": { + "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_created_by_user_id_fk": { + "name": "credential_group_enrollment_created_by_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_enrollment_normalized_email_check": { + "name": "credential_group_enrollment_normalized_email_check", + "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320" + }, + "credential_group_enrollment_invitation_token_hash_length_check": { + "name": "credential_group_enrollment_invitation_token_hash_length_check", + "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trace_child_runs": { + "name": "trace_child_runs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_active_kb_token_count_idx": { + "name": "doc_active_kb_token_count_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "token_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_started_at_idx": { + "name": "kcsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_byok_keys": { + "name": "organization_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_byok_organization_provider_idx": { + "name": "organization_byok_organization_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_byok_keys_organization_id_organization_id_fk": { + "name": "organization_byok_keys_organization_id_organization_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_byok_keys_created_by_user_id_fk": { + "name": "organization_byok_keys_created_by_user_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_item": { + "name": "pinned_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_usage": { + "name": "secret_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_name": { + "name": "secret_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_scope": { + "name": "secret_scope", + "type": "secret_usage_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret_owner_user_id": { + "name": "secret_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "source": { + "name": "source", + "type": "secret_usage_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_execution_id": { + "name": "last_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_trigger": { + "name": "last_trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_usage_bucket_unique": { + "name": "secret_usage_bucket_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_usage_secret_recent_idx": { + "name": "secret_usage_secret_recent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_usage_workspace_id_workspace_id_fk": { + "name": "secret_usage_workspace_id_workspace_id_fk", + "tableFrom": "secret_usage", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_focus_on_click": { + "name": "auto_focus_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "last_closed_period_start": { + "name": "last_closed_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_workspace_created_idx": { + "name": "table_views_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.upload_session": { + "name": "upload_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "upload_session_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "upload_session_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_context": { + "name": "storage_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "final_key": { + "name": "final_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "upload_session_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_object_version": { + "name": "provider_object_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "part_count": { + "name": "part_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "upload_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_lease_id": { + "name": "processing_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_lease_expires_at": { + "name": "processing_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_file_id": { + "name": "completed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "upload_session_token_hash_unique": { + "name": "upload_session_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_final_key_unique": { + "name": "upload_session_final_key_unique", + "columns": [ + { + "expression": "final_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_status_expires_at_idx": { + "name": "upload_session_status_expires_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_active_workspace_sort_idx": { + "name": "workflow_active_workspace_sort_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_enabled": { + "name": "error_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retry": { + "name": "retry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_started_at_idx": { + "name": "workflow_execution_logs_redacting_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'redacting'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_deadline_idx": { + "name": "workflow_execution_logs_redacting_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_inbox_provider_id_idx": { + "name": "workspace_inbox_provider_id_idx", + "columns": [ + { + "expression": "inbox_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_workspace_idx": { + "name": "workspace_sandbox_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_group_enrollment_status": { + "name": "credential_group_enrollment_status", + "schema": "public", + "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"] + }, + "public.credential_group_status": { + "name": "credential_group_status", + "schema": "public", + "values": ["active", "disabled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "managed_oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.managed_oauth_credential_status": { + "name": "managed_oauth_credential_status", + "schema": "public", + "values": ["active", "needs_reauth", "revoked"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.secret_usage_scope": { + "name": "secret_usage_scope", + "schema": "public", + "values": ["workspace", "personal"] + }, + "public.secret_usage_source": { + "name": "secret_usage_source", + "schema": "public", + "values": ["workflow", "copilot", "mcp"] + }, + "public.upload_session_method": { + "name": "upload_session_method", + "schema": "public", + "values": ["put", "multipart"] + }, + "public.upload_session_provider": { + "name": "upload_session_provider", + "schema": "public", + "values": ["local", "s3", "blob", "gcs"] + }, + "public.upload_session_purpose": { + "name": "upload_session_purpose", + "schema": "public", + "values": [ + "workspace_file", + "table_import", + "knowledge_document", + "profile_picture", + "workspace_logo", + "mothership_attachment", + "execution_attachment" + ] + }, + "public.upload_session_status": { + "name": "upload_session_status", + "schema": "public", + "values": [ + "uploading", + "completing", + "finalizing", + "completed", + "aborting", + "aborted", + "failed", + "expired" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment", + "voice-output" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "workflow_mcp_server", + "custom_block", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 82d62c563ed..d704f4f261d 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2129,6 +2129,13 @@ "when": 1787598098228, "tag": "0304_slippery_carmella_unuscione", "breakpoints": true + }, + { + "idx": 305, + "version": "7", + "when": 1787687900983, + "tag": "0305_add_subscription_last_closed_period_start", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index a53c47e25a0..018aad5c80d 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1196,23 +1196,22 @@ export const userStats = pgTable('user_stats', { totalCost: decimal('total_cost').notNull().default('0'), currentUsageLimit: decimal('current_usage_limit').default(DEFAULT_FREE_CREDITS.toString()), // Default $5 (1,000 credits) for free plan, null for team/enterprise usageLimitUpdatedAt: timestamp('usage_limit_updated_at').defaultNow(), - /** - * Active per-period baseline (not a per-usage hot-path counter). Current usage - * = this baseline + attributed usage_log rows for the period; reset at rollover. - */ + /** @deprecated No readers or writers; usage is the attributed usage_log ledger. Drop via DROP COLUMN in a follow-up migration. */ currentPeriodCost: decimal('current_period_cost').notNull().default('0'), - lastPeriodCost: decimal('last_period_cost').default('0'), // Usage from previous billing period + /** Previous-period usage; written by the cycle-close sweep from ledger sums. */ + lastPeriodCost: decimal('last_period_cost').default('0'), /** * Threshold/final billing tracker. * - * This is intentionally still written when threshold billing or invoice - * finalization serializes overage collection. It is not incremented by the - * ordinary per-usage ledger write path. + * Incremented when threshold billing collects overage mid-period; reset to + * zero by the cycle-close sweep at period rollover. It is not incremented + * by the ordinary per-usage ledger write path. */ billedOverageThisPeriod: decimal('billed_overage_this_period').notNull().default('0'), // Amount of overage already billed via threshold billing - // Pro usage snapshot when joining a team (to prevent double-billing) - proPeriodCostSnapshot: decimal('pro_period_cost_snapshot').default('0'), // Snapshot of Pro usage when joining team - proPeriodCostSnapshotAt: timestamp('pro_period_cost_snapshot_at'), // When the snapshot was captured (= join moment). Used to cap daily-refresh computation so post-join refresh isn't deducted from pre-join personal Pro usage (and vice-versa for the org's pooled refresh). + /** @deprecated No readers or writers; ledger entity stamps attribute pre/post-join usage. Drop via DROP COLUMN in a follow-up migration. */ + proPeriodCostSnapshot: decimal('pro_period_cost_snapshot').default('0'), + /** @deprecated No readers or writers; see proPeriodCostSnapshot. Drop via DROP COLUMN in a follow-up migration. */ + proPeriodCostSnapshotAt: timestamp('pro_period_cost_snapshot_at'), /** * Credit balance tracker. * @@ -1222,9 +1221,9 @@ export const userStats = pgTable('user_stats', { creditBalance: decimal('credit_balance').notNull().default('0'), /** @deprecated Not written; report Copilot cost from usage_log. Legacy/admin reads only. */ totalCopilotCost: decimal('total_copilot_cost').notNull().default('0'), - /** Active per-period Copilot baseline; reset at rollover (not a per-usage counter). */ + /** @deprecated No readers or writers; Copilot usage is the copilot-source usage_log ledger. Drop via DROP COLUMN in a follow-up migration. */ currentPeriodCopilotCost: decimal('current_period_copilot_cost').notNull().default('0'), - /** Previous-period Copilot cost; set at rollover. */ + /** Previous-period Copilot cost; written by the cycle-close sweep from copilot-source ledger sums. */ lastPeriodCopilotCost: decimal('last_period_copilot_cost').default('0'), /** @deprecated Not written; report Copilot tokens from usage_log. Legacy/admin reads only. */ totalCopilotTokens: bigint('total_copilot_tokens', { mode: 'number' }).notNull().default(0), @@ -1362,6 +1361,16 @@ export const subscription = pgTable( billingInterval: text('billing_interval'), stripeScheduleId: text('stripe_schedule_id'), metadata: json('metadata'), + /** + * Durable cycle-close marker: the `periodStart` of the most recent period + * whose close (final overage collection, `billedOverageThisPeriod` reset, + * last-period bookkeeping) has been committed. The daily cycle-close sweep + * closes the previous period whenever this lags the row's `periodStart`, + * then advances it. Null = never initialized; the first sweep initializes + * it to the current `periodStart` without billing so historical periods + * are never retroactively closed. + */ + lastClosedPeriodStart: timestamp('last_closed_period_start'), }, (table) => ({ referenceStatusIdx: index('subscription_reference_status_idx').on( @@ -1582,6 +1591,7 @@ export const organization = pgTable('organization', { .$type>() .notNull() .default({}), + /** @deprecated No readers or writers; a departed member's ledger rows stay stamped to the org's period, so nothing needs capturing. Drop via DROP COLUMN in a follow-up migration. */ departedMemberUsage: decimal('departed_member_usage').notNull().default('0'), /** * Organization credit balance tracker. diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index 068a12f6944..061063a1627 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -520,6 +520,7 @@ export const schemaMock = { trialStart: 'subscription.trialStart', trialEnd: 'subscription.trialEnd', metadata: 'subscription.metadata', + lastClosedPeriodStart: 'subscription.lastClosedPeriodStart', }, rateLimitBucket: { key: 'rateLimitBucket.key', diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index dc92dbc762f..a5fb0b221f9 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -89,6 +89,7 @@ const INDIRECT_ZOD_ROUTES = new Set([ 'apps/sim/app/api/cron/cleanup-stale-executions/route.ts', 'apps/sim/app/api/cron/cleanup-sandbox-images/route.ts', 'apps/sim/app/api/cron/renew-subscriptions/route.ts', + 'apps/sim/app/api/cron/billing-cycle-close/route.ts', 'apps/sim/app/api/cron/reconcile-billing-seats/route.ts', 'apps/sim/app/api/cron/reconcile-inbox-entitlement/route.ts', 'apps/sim/app/api/cron/run-data-drains/route.ts',