Files
supabase/apps/studio/data/storage/versioning/object-versions-query.test.ts
T
Francesco SansalvadoreandClaude Sonnet 5 2d0bd7af69 feat(storage): add object versions data layer (#49207)
| # | Branch | Base |
| - | ------ | ---- |
| 1 | `feat/storage-versioning-private-alpha` — merged | `master` |
| 2 | `feat/storage-versioning/002-bucket-form-fields` | `master` |
| 3 | `feat/storage-versioning/003-bucket-modals` | 2 |
| 4 | `feat/storage-versioning/004-object-versions-data` ◀ | 3 |
| 5 | `feat/storage-versioning/005-file-preview-versions` | 4 |
| 6 | `feat/storage-versioning/006-billing-storage-retention` | 5 |
| 7 | `feat/storage-versioning/007-archived-objects-data` | 6 |
| 8 | `feat/storage-versioning/008-archived-rows` | 7 |
| 9 | `feat/storage-versioning/009-archived-preview-pane` | 8 |
| 10 | `feat/storage-versioning/010-replace-file` | 9 |

## [4/10] Storage object versioning: object versions data layer

**Base:** `feat/storage-versioning/003-bucket-modals` (PR 3)

### This PR

The query and mutation hooks for the version history UI, written to
`queryOptions` using the real Storage endpoints.

- `object-versions-query.ts` — the version list, plus `ObjectVersion`
and `LifecyclePolicy`
- `object-version-restore-mutation.ts` — promote a noncurrent version to
current
- `object-version-delete-mutation.ts` — remove one specific version
- `object-purge-mutation.ts` — delete an object and every version,
bypassing versioning
- `VersionHistory.utils.ts` — `computeVersionFate`, the pure rule
deciding what removal outlook each version row shows
- `BroomSparklesIcon.tsx` — inline SVG for a glyph absent from
lucide-react 0.436

Easier to test directly from next PR in the stack #49208 which wires the
queries to the real file preview panel ui.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-07 22:12:19 +02:00

94 lines
3.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { toObjectVersions } from './object-versions-query'
type StorageObject = Parameters<typeof toObjectVersions>[0][number]
const object = (overrides: Partial<StorageObject>): StorageObject => ({
name: 'folder/photo.jpg',
id: null,
updated_at: null,
created_at: null,
last_accessed_at: null,
metadata: { size: 100 },
...overrides,
})
describe('toObjectVersions', () => {
it('returns newest first', () => {
const versions = toObjectVersions([
object({ version: 'old', created_at: '2026-01-01T00:00:00Z', archived_at: '2026-02-01' }),
object({ version: 'new', created_at: '2026-03-01T00:00:00Z' }),
object({ version: 'mid', created_at: '2026-02-01T00:00:00Z', archived_at: '2026-03-01' }),
])
expect(versions.map((version) => version.versionId)).toEqual(['new', 'mid', 'old'])
})
it('treats a row with no archived_at as the current version', () => {
const versions = toObjectVersions([
object({ version: 'a', created_at: '2026-01-01T00:00:00Z' }),
object({ version: 'b', created_at: '2026-02-01T00:00:00Z', archived_at: '2026-03-01' }),
])
expect(versions.find((version) => version.versionId === 'a')?.isCurrent).toBe(true)
expect(versions.find((version) => version.versionId === 'b')?.isCurrent).toBe(false)
})
it('labels the oldest upload and treats the rest as overwrites', () => {
const versions = toObjectVersions([
object({ version: 'a', created_at: '2026-01-01T00:00:00Z' }),
object({ version: 'b', created_at: '2026-02-01T00:00:00Z' }),
object({ version: 'c', created_at: '2026-03-01T00:00:00Z' }),
])
expect(versions.map((version) => version.action)).toEqual([
'overwrite',
'overwrite',
'initial upload',
])
})
it('labels delete markers and does not count them as the initial upload', () => {
const versions = toObjectVersions([
object({ version: 'upload', created_at: '2026-01-01T00:00:00Z' }),
object({
version: 'marker',
created_at: '2026-02-01T00:00:00Z',
is_delete_marker: true,
metadata: null,
}),
])
expect(versions.map((version) => version.action)).toEqual(['delete marker', 'initial upload'])
})
it('reads the size off the metadata, defaulting to zero', () => {
const versions = toObjectVersions([
object({ version: 'a', created_at: '2026-01-01T00:00:00Z', metadata: { size: 2048 } }),
object({ version: 'b', created_at: '2026-02-01T00:00:00Z', metadata: null }),
])
expect(versions.find((version) => version.versionId === 'a')?.size).toBe(2048)
expect(versions.find((version) => version.versionId === 'b')?.size).toBe(0)
})
it('keeps each version its own mime type, since an overwrite can change it', () => {
const versions = toObjectVersions([
object({
version: 'a',
created_at: '2026-01-01T00:00:00Z',
metadata: { mimetype: 'image/png' },
}),
object({ version: 'b', created_at: '2026-02-01T00:00:00Z', metadata: { size: 1 } }),
])
expect(versions.find((version) => version.versionId === 'a')?.mimeType).toBe('image/png')
expect(versions.find((version) => version.versionId === 'b')?.mimeType).toBeUndefined()
})
it('drops rows the API returned without a version id', () => {
expect(toObjectVersions([object({ created_at: '2026-01-01T00:00:00Z' })])).toEqual([])
})
})