Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 106 additions & 27 deletions packages/core/storage-js/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,34 +44,103 @@ export interface AnalyticBucket {
updated_at: string
}

/**
* Metadata object returned by the Storage API for files
* Contains information about file size, type, caching, and HTTP response details
*/
export interface FileMetadata {
/** Entity tag for caching and conditional requests */
eTag: string
/** File size in bytes */
size: number
/** MIME type of the file */
mimetype: string
/** Cache control directive (e.g., "max-age=3600") */
cacheControl: string
/** Last modification timestamp (ISO 8601) */
lastModified: string
/** Content length in bytes (usually same as size) */
contentLength: number
/** HTTP status code from the storage backend */
httpStatusCode: number
/** Any additional custom metadata stored with the file */
[key: string]: any
}

/**
* File object returned by the List V1 API (list() method)
* Note: Folder entries will have null values for most fields except name
*
* Warning: Some fields may not be present in all API responses. Fields like
* bucket_id, owner, and buckets are not returned by list() operations.
*/
export interface FileObject {
/** File or folder name (relative to the prefix) - always present */
name: string
bucket_id: string
owner: string
id: string
updated_at: string
created_at: string
/** @deprecated */
last_accessed_at: string
metadata: Record<string, any>
buckets: Bucket
/** Unique identifier for the file (null for folders) */
id: string | null
/** Last update timestamp (null for folders) */
updated_at: string | null
/** Creation timestamp (null for folders) */
created_at: string | null
/** @deprecated Last access timestamp (null for folders) */
last_accessed_at: string | null
/** File metadata including size, mimetype, etc. (null for folders) */
metadata: FileMetadata | null
/**
* @deprecated Bucket identifier - NOT returned by list() operations.
* May be present in remove() responses. Do not rely on this field.
*/
bucket_id?: string
/**
* @deprecated Owner identifier - NOT returned by list() or remove() operations.
* This field should not be relied upon.
*/
owner?: string
/**
* @deprecated Bucket object - NOT returned by list() or remove() operations.
* This field should not be relied upon.
*/
buckets?: Bucket
}

/**
* File object returned by the Info endpoint (info() method)
* Contains detailed metadata for a specific file
*
* Note: The info endpoint returns user_metadata as the metadata field,
* while system metadata (size, mimetype, etc.) is flattened into top-level fields.
*/
export interface FileObjectV2 {
/** Unique identifier for the file */
id: string
/** File version identifier */
version: string
/** File name */
name: string
/** Bucket identifier */
bucket_id: string
updated_at: string
/** Last modification timestamp */
last_modified: string
/** Creation timestamp */
created_at: string
/** @deprecated */
last_accessed_at: string
size?: number
cache_control?: string
content_type?: string
etag?: string
last_modified?: string
metadata?: Record<string, any>
/** @deprecated Use last_modified instead. Not returned by info endpoint. */
last_accessed_at?: string
/** File size in bytes (null if not available) */
size: number | null
/** Cache control header value (null if not set) */
cache_control: string | null
/** MIME content type (null if not available) */
content_type: string | null
/** Entity tag for caching (null if not available) */
etag: string | null
/** User-provided custom metadata (arbitrary key-value pairs) */
metadata: Record<string, any> | null
/**
* @deprecated The API returns last_modified instead.
* This field may not be present in responses.
*/
updated_at?: string
}

export interface SortBy {
Expand Down Expand Up @@ -175,17 +244,25 @@ export interface SearchV2Options {
sortBy?: SortByV2
}

/**
* File object returned by the List V2 API (listV2() method)
* Note: Folder entries will have null values for most fields except key and name
*/
export interface SearchV2Object {
id: string
key: string
/** File or folder name - always present */
name: string
updated_at: string
created_at: string
metadata: Record<string, any>
/**
* @deprecated
*/
last_accessed_at: string
/** Full object key/path (may be missing in some responses) */
key?: string
/** Unique identifier for the file (null for folders) */
id: string | null
/** Last update timestamp (null for folders) */
updated_at: string | null
/** Creation timestamp (null for folders) */
created_at: string | null
/** File metadata (null for folders) */
metadata: FileMetadata | null
/** @deprecated Last access timestamp (null for folders) */
last_accessed_at: string | null
}

export type SearchV2Folder = Omit<SearchV2Object, 'id' | 'metadata' | 'last_accessed_at'>
Expand All @@ -195,6 +272,8 @@ export interface SearchV2Result {
folders: SearchV2Folder[]
objects: SearchV2Object[]
nextCursor?: string
/** The key/name used for cursor-based pagination (returned by storage server) */
nextCursorKey?: string
}

export interface FetchParameters {
Expand Down
75 changes: 71 additions & 4 deletions packages/core/storage-js/src/packages/StorageFileApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,9 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
/**
* Retrieves the details of an existing file.
*
* Returns detailed file metadata including size, content type, and timestamps.
* Note: The API returns `last_modified` field, not `updated_at`.
*
* @category File Buckets
* @param path The file path, including the file name. For example `folder/image.png`.
* @returns Promise with response containing file metadata or error
Expand All @@ -775,6 +778,11 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
* .storage
* .from('avatars')
* .info('folder/avatar1.png')
*
* if (data) {
* console.log('Last modified:', data.lastModified)
* console.log('Size:', data.size)
* }
* ```
*/
async info(path: string): Promise<
Expand Down Expand Up @@ -933,6 +941,9 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
/**
* Deletes files within the same bucket
*
* Returns an array of FileObject entries for the deleted files. Note that deprecated
* fields like `bucket_id` may or may not be present in the response - do not rely on them.
*
* @category File Buckets
* @param paths An array of files to delete, including the path and file name. For example [`'folder/image.png'`].
* @returns Promise with response containing array of deleted file objects or error
Expand Down Expand Up @@ -1039,11 +1050,16 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
/**
* Lists all the files and folders within a path of the bucket.
*
* **Important:** For folder entries, fields like `id`, `updated_at`, `created_at`,
* `last_accessed_at`, and `metadata` will be `null`. Only files have these fields populated.
* Additionally, deprecated fields like `bucket_id`, `owner`, and `buckets` are NOT returned
* by this method.
*
* @category File Buckets
* @param path The folder path.
* @param options Search options including limit (defaults to 100), offset, sortBy, and search
* @param parameters Optional fetch parameters including signal for cancellation
* @returns Promise with response containing array of files or error
* @returns Promise with response containing array of files/folders or error
*
* @example List files in a bucket
* ```js
Expand All @@ -1055,9 +1071,20 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
* offset: 0,
* sortBy: { column: 'name', order: 'asc' },
* })
*
* // Handle files vs folders
* data?.forEach(item => {
* if (item.id !== null) {
* // It's a file
* console.log('File:', item.name, 'Size:', item.metadata?.size)
* } else {
* // It's a folder
* console.log('Folder:', item.name)
* }
* })
* ```
*
* Response:
* Response (file entry):
* ```json
* {
* "data": [
Expand Down Expand Up @@ -1122,11 +1149,51 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
}

/**
* Lists all the files and folders within a bucket using the V2 API with pagination support.
*
* **Important:** For folder entries in the `folders` array, fields like `id`, `updated_at`,
* `created_at`, `last_accessed_at`, and `metadata` will be `null`. Only files in the
* `objects` array have these fields populated. The `key` field may also be missing in
* some responses.
*
* @experimental this method signature might change in the future
*
* @category File Buckets
* @param options search options
* @param parameters
* @param options Search options including prefix, cursor for pagination, limit, with_delimiter
* @param parameters Optional fetch parameters including signal for cancellation
* @returns Promise with response containing folders/objects arrays with pagination info or error
*
* @example List files with pagination
* ```js
* const { data, error } = await supabase
* .storage
* .from('avatars')
* .listV2({
* prefix: 'folder/',
* limit: 100,
* })
*
* // Handle pagination
* if (data?.hasNext) {
* const nextPage = await supabase
* .storage
* .from('avatars')
* .listV2({
* prefix: 'folder/',
* cursor: data.nextCursor,
* })
* }
*
* // Handle files vs folders
* data?.objects.forEach(file => {
* if (file.id !== null) {
* console.log('File:', file.name, 'Size:', file.metadata?.size)
* }
* })
* data?.folders.forEach(folder => {
* console.log('Folder:', folder.name)
* })
* ```
*/
async listV2(
options?: SearchV2Options,
Expand Down
45 changes: 41 additions & 4 deletions packages/core/storage-js/test/storageFileApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import { StorageApiError, StorageError } from '../src/lib/common/errors'
import BlobDownloadBuilder from '../src/packages/BlobDownloadBuilder'
import StreamDownloadBuilder from '../src/packages/StreamDownloadBuilder'

// Type assertion helper for test responses
function assertSuccess<T>(res: {
data: T | null
error: StorageError | null
}): asserts res is { data: T; error: null } {
expect(res.error).toBeNull()
expect(res.data).not.toBeNull()
}

// Supabase CLI local development defaults
const URL = 'http://127.0.0.1:54321/storage/v1'
// service_role key - bypasses RLS for testing
Expand Down Expand Up @@ -332,12 +341,21 @@ describe('Object API', () => {
test('list objects', async () => {
await storage.from(bucketName).upload(uploadPath, file)
const res = await storage.from(bucketName).list('testpath')
expect(res.error).toBeNull()
assertSuccess(res)
expect(res.data).toEqual([
expect.objectContaining({
name: uploadPath.replace('testpath/', ''),
id: expect.any(String), // Files should have non-null id
metadata: expect.any(Object), // Files should have metadata
}),
])

// Verify files have non-null required fields
const fileObj = res.data[0]
expect(fileObj.id).not.toBeNull()
expect(fileObj.metadata).not.toBeNull()
expect(fileObj.updated_at).not.toBeNull()
expect(fileObj.created_at).not.toBeNull()
})

test('list objects V2', async () => {
Expand Down Expand Up @@ -517,20 +535,26 @@ describe('Object API', () => {
await storage.from(bucketName).upload(uploadPath, file)
const res = await storage.from(bucketName).remove([uploadPath])

expect(res.error).toBeNull()
assertSuccess(res)
expect(res.data).toEqual([
expect.objectContaining({
bucket_id: bucketName,
name: uploadPath,
id: expect.any(String), // Verify it's a file, not a folder
}),
])

// bucket_id may be present in remove() responses (deprecated field)
// If present, verify it matches
if (res.data[0].bucket_id) {
expect(res.data[0].bucket_id).toBe(bucketName)
}
})

test('get object info', async () => {
await storage.from(bucketName).upload(uploadPath, file)
const res = await storage.from(bucketName).info(uploadPath)

expect(res.error).toBeNull()
assertSuccess(res)
expect(res.data).toEqual(
expect.objectContaining({
id: expect.any(String),
Expand All @@ -546,6 +570,19 @@ describe('Object API', () => {
})
)

// Verify FileObjectV2 required fields
expect(res.data.id).toBeDefined()
expect(res.data.bucketId).toBeDefined()
expect(res.data.lastModified).toBeDefined() // Should have this
expect(res.data.size).toBeGreaterThan(0)
expect(res.data.contentType).toBeDefined()
expect(res.data.cacheControl).toBeDefined()
expect(res.data.etag).toBeDefined()

// Verify updated_at does NOT exist (API returns camelCase, but the raw type shouldn't have it)
// Note: The info() method uses Camelize so we check the camelCase version
expect(res.data).not.toHaveProperty('updatedAt')

// throws when .throwOnError is enabled
await expect(storage.from(bucketName).throwOnError().info('non-existent')).rejects.toThrow()
})
Expand Down
Loading