Files
supabase/apps/studio/components/interfaces/Storage/StorageProtection.constants.ts
T
Claude 889ab611a6 make versioning number inputs deletable, enforce plan-based limits
- Move versioning fields (retention days, max noncurrent versions) from
  local useState into the parent form via react-hook-form + zod, fixing
  the bug where clearing an input forced it back to "0" and blocked
  typing a fresh single digit
- Show a validation error on submit if versioning is enabled but a
  field is left empty
- Gate object versioning by org billing plan: disable the toggle and
  show an upgrade prompt on the Free plan (which also doesn't support
  lifecycle policy management); enforce plan-specific min/max ranges
  for retention days and max versions on Pro/Team/Enterprise
- Add getVersioningPlanLimits() + validateVersioningFields() helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
2026-08-05 13:28:40 +00:00

99 lines
2.9 KiB
TypeScript

/**
* Shared constants + gating for the Storage Object Versioning prototype.
*/
// Prototype flag: force-enabled so the surfaces are visible via `pnpm dev:studio`.
// A real rollout would replace this with `useFlag('storageObjectVersioning')`.
export const STORAGE_PROTECTION_ENABLED = true
export const useIsStorageProtectionEnabled = () => STORAGE_PROTECTION_ENABLED
export type BucketVersioningState = 'enabled' | 'suspended' | 'disabled'
export const PROJECT_VERSIONING_DEFAULTS = {
versionExpiryDays: 30,
maxNoncurrentVersions: 100,
} as const
/**
* Sensible min/max/default bounds for the versioning fields, per org billing plan.
* `null` means the plan doesn't support object versioning (or lifecycle policy
* management) at all.
*/
export interface VersioningPlanLimits {
minRetentionDays: number
maxRetentionDays: number
defaultRetentionDays: number
minVersions: number
maxVersions: number
defaultVersions: number
}
const VERSIONING_PLAN_LIMITS: Record<string, VersioningPlanLimits | null> = {
free: null,
pro: {
minRetentionDays: 1,
maxRetentionDays: 90,
defaultRetentionDays: 30,
minVersions: 1,
maxVersions: 20,
defaultVersions: 10,
},
team: {
minRetentionDays: 1,
maxRetentionDays: 180,
defaultRetentionDays: 30,
minVersions: 1,
maxVersions: 50,
defaultVersions: 20,
},
enterprise: {
minRetentionDays: 1,
maxRetentionDays: 365,
defaultRetentionDays: 30,
minVersions: 1,
maxVersions: 100,
defaultVersions: 50,
},
}
// Legacy/custom org tier: treat the same as enterprise (most generous bounds)
VERSIONING_PLAN_LIMITS.platform = VERSIONING_PLAN_LIMITS.enterprise
/**
* Returns the versioning min/max bounds for a given org plan, or `null` if
* that plan doesn't support object versioning (e.g. the Free plan).
*/
export const getVersioningPlanLimits = (planId: string | undefined): VersioningPlanLimits | null =>
planId !== undefined ? (VERSIONING_PLAN_LIMITS[planId] ?? null) : null
export interface BucketProtection {
versioning: BucketVersioningState
versionExpiryDays: number | null
maxNoncurrentVersions: number | null
}
const PROTECTED_BUCKETS: Record<string, BucketProtection> = {
'match-media': {
versioning: 'enabled',
versionExpiryDays: PROJECT_VERSIONING_DEFAULTS.versionExpiryDays,
maxNoncurrentVersions: PROJECT_VERSIONING_DEFAULTS.maxNoncurrentVersions,
},
avatars: {
versioning: 'enabled',
versionExpiryDays: 30,
maxNoncurrentVersions: 10,
},
}
const DEFAULT_PROTECTION: BucketProtection = {
versioning: 'disabled',
versionExpiryDays: null,
maxNoncurrentVersions: null,
}
export const getMockBucketProtection = (bucketName: string | undefined): BucketProtection =>
(bucketName && PROTECTED_BUCKETS[bucketName]) || DEFAULT_PROTECTION
export const isBucketVersioned = (bucketName: string | undefined) =>
getMockBucketProtection(bucketName).versioning === 'enabled'