Skip to content

Commit c338e2f

Browse files
authored
fix(storage): improve FileObject type accuracy with nullable fields (supabase#2116)
1 parent 8d0119f commit c338e2f

File tree

3 files changed

+203
-25
lines changed

3 files changed

+203
-25
lines changed

packages/core/storage-js/src/lib/types.ts

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,34 +44,98 @@ export interface AnalyticBucket {
4444
updated_at: string
4545
}
4646

47+
/**
48+
* Metadata object returned by the Storage API for files
49+
* Contains information about file size, type, caching, and HTTP response details
50+
*/
51+
export interface FileMetadata {
52+
/** Entity tag for caching and conditional requests */
53+
eTag: string
54+
/** File size in bytes */
55+
size: number
56+
/** MIME type of the file */
57+
mimetype: string
58+
/** Cache control directive (e.g., "max-age=3600") */
59+
cacheControl: string
60+
/** Last modification timestamp (ISO 8601) */
61+
lastModified: string
62+
/** Content length in bytes (usually same as size) */
63+
contentLength: number
64+
/** HTTP status code from the storage backend */
65+
httpStatusCode: number
66+
/** Any additional custom metadata stored with the file */
67+
[key: string]: any
68+
}
69+
70+
/**
71+
* File object returned by the List V1 API (list() method)
72+
* Note: Folder entries will have null values for most fields except name
73+
*
74+
* Warning: Some fields may not be present in all API responses. Fields like
75+
* bucket_id, owner, and buckets are not returned by list() operations.
76+
*/
4777
export interface FileObject {
78+
/** File or folder name (relative to the prefix) - always present */
4879
name: string
49-
bucket_id: string
50-
owner: string
51-
id: string
52-
updated_at: string
53-
created_at: string
54-
/** @deprecated */
55-
last_accessed_at: string
56-
metadata: Record<string, any>
57-
buckets: Bucket
80+
/** Unique identifier for the file (null for folders) */
81+
id: string | null
82+
/** Last update timestamp (null for folders) */
83+
updated_at: string | null
84+
/** Creation timestamp (null for folders) */
85+
created_at: string | null
86+
/** @deprecated Last access timestamp (null for folders) */
87+
last_accessed_at: string | null
88+
/** File metadata including size, mimetype, etc. (null for folders) */
89+
metadata: FileMetadata | null
90+
/**
91+
* @deprecated Bucket identifier - NOT returned by list() operations.
92+
* May be present in remove() responses. Do not rely on this field.
93+
*/
94+
bucket_id?: string
95+
/**
96+
* @deprecated Owner identifier - NOT returned by list() or remove() operations.
97+
* This field should not be relied upon.
98+
*/
99+
owner?: string
100+
/**
101+
* @deprecated Bucket object - NOT returned by list() or remove() operations.
102+
* This field should not be relied upon.
103+
*/
104+
buckets?: Bucket
58105
}
59106

107+
/**
108+
* File object returned by the Info endpoint (info() method)
109+
* Contains detailed metadata for a specific file
110+
*/
60111
export interface FileObjectV2 {
112+
/** Unique identifier for the file */
61113
id: string
114+
/** File version identifier */
62115
version: string
116+
/** File name */
63117
name: string
118+
/** Bucket identifier */
64119
bucket_id: string
65-
updated_at: string
120+
/** Creation timestamp */
66121
created_at: string
67-
/** @deprecated */
68-
last_accessed_at: string
122+
/** File size in bytes */
69123
size?: number
124+
/** Cache control header value */
70125
cache_control?: string
126+
/** MIME content type */
71127
content_type?: string
128+
/** Entity tag for caching */
72129
etag?: string
130+
/** Last modification timestamp (replaces updated_at) */
73131
last_modified?: string
74-
metadata?: Record<string, any>
132+
/** Custom file metadata */
133+
metadata?: FileMetadata
134+
/**
135+
* @deprecated The API returns last_modified instead.
136+
* This field may not be present in responses.
137+
*/
138+
updated_at?: string
75139
}
76140

77141
export interface SortBy {
@@ -175,20 +239,37 @@ export interface SearchV2Options {
175239
sortBy?: SortByV2
176240
}
177241

242+
/**
243+
* File object returned by the List V2 API (listV2() method)
244+
* Objects and folders are returned in separate arrays - this type represents
245+
* actual files only. Use SearchV2Folder for folder entries.
246+
*/
178247
export interface SearchV2Object {
179-
id: string
180-
key: string
248+
/** File name */
181249
name: string
250+
/** Full object key/path */
251+
key?: string
252+
/** Unique identifier for the file */
253+
id: string
254+
/** Last update timestamp */
182255
updated_at: string
256+
/** Creation timestamp */
183257
created_at: string
184-
metadata: Record<string, any>
185-
/**
186-
* @deprecated
187-
*/
258+
/** File metadata including size, mimetype, etc. (null if not yet set) */
259+
metadata: FileMetadata | null
260+
/** @deprecated Last access timestamp */
188261
last_accessed_at: string
189262
}
190263

191-
export type SearchV2Folder = Omit<SearchV2Object, 'id' | 'metadata' | 'last_accessed_at'>
264+
/**
265+
* Folder entry returned by the List V2 API (listV2() method) when using with_delimiter: true
266+
*/
267+
export interface SearchV2Folder {
268+
/** Folder name/prefix */
269+
name: string
270+
/** Full folder key/path */
271+
key?: string
272+
}
192273

193274
export interface SearchV2Result {
194275
hasNext: boolean

packages/core/storage-js/src/packages/StorageFileApi.ts

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,9 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
777777
/**
778778
* Retrieves the details of an existing file.
779779
*
780+
* Returns detailed file metadata including size, content type, and timestamps.
781+
* Note: The API returns `last_modified` field, not `updated_at`.
782+
*
780783
* @category File Buckets
781784
* @param path The file path, including the file name. For example `folder/image.png`.
782785
* @returns Promise with response containing file metadata or error
@@ -787,6 +790,11 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
787790
* .storage
788791
* .from('avatars')
789792
* .info('folder/avatar1.png')
793+
*
794+
* if (data) {
795+
* console.log('Last modified:', data.lastModified)
796+
* console.log('Size:', data.size)
797+
* }
790798
* ```
791799
*/
792800
async info(path: string): Promise<
@@ -951,6 +959,9 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
951959
/**
952960
* Deletes files within the same bucket
953961
*
962+
* Returns an array of FileObject entries for the deleted files. Note that deprecated
963+
* fields like `bucket_id` may or may not be present in the response - do not rely on them.
964+
*
954965
* @category File Buckets
955966
* @param paths An array of files to delete, including the path and file name. For example [`'folder/image.png'`].
956967
* @returns Promise with response containing array of deleted file objects or error
@@ -1057,11 +1068,16 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
10571068
/**
10581069
* Lists all the files and folders within a path of the bucket.
10591070
*
1071+
* **Important:** For folder entries, fields like `id`, `updated_at`, `created_at`,
1072+
* `last_accessed_at`, and `metadata` will be `null`. Only files have these fields populated.
1073+
* Additionally, deprecated fields like `bucket_id`, `owner`, and `buckets` are NOT returned
1074+
* by this method.
1075+
*
10601076
* @category File Buckets
10611077
* @param path The folder path.
10621078
* @param options Search options including limit (defaults to 100), offset, sortBy, and search
10631079
* @param parameters Optional fetch parameters including signal for cancellation
1064-
* @returns Promise with response containing array of files or error
1080+
* @returns Promise with response containing array of files/folders or error
10651081
*
10661082
* @example List files in a bucket
10671083
* ```js
@@ -1073,9 +1089,20 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
10731089
* offset: 0,
10741090
* sortBy: { column: 'name', order: 'asc' },
10751091
* })
1092+
*
1093+
* // Handle files vs folders
1094+
* data?.forEach(item => {
1095+
* if (item.id !== null) {
1096+
* // It's a file
1097+
* console.log('File:', item.name, 'Size:', item.metadata?.size)
1098+
* } else {
1099+
* // It's a folder
1100+
* console.log('Folder:', item.name)
1101+
* }
1102+
* })
10761103
* ```
10771104
*
1078-
* Response:
1105+
* Response (file entry):
10791106
* ```json
10801107
* {
10811108
* "data": [
@@ -1140,11 +1167,50 @@ export default class StorageFileApi extends BaseApiClient<StorageError> {
11401167
}
11411168

11421169
/**
1170+
* Lists all the files and folders within a bucket using the V2 API with pagination support.
1171+
*
1172+
* **Important:** Folder entries in the `folders` array only contain `name` and optionally `key` —
1173+
* they have no `id`, timestamps, or `metadata` fields. Full file metadata is only available
1174+
* on entries in the `objects` array.
1175+
*
11431176
* @experimental this method signature might change in the future
11441177
*
11451178
* @category File Buckets
1146-
* @param options search options
1147-
* @param parameters
1179+
* @param options Search options including prefix, cursor for pagination, limit, with_delimiter
1180+
* @param parameters Optional fetch parameters including signal for cancellation
1181+
* @returns Promise with response containing folders/objects arrays with pagination info or error
1182+
*
1183+
* @example List files with pagination
1184+
* ```js
1185+
* const { data, error } = await supabase
1186+
* .storage
1187+
* .from('avatars')
1188+
* .listV2({
1189+
* prefix: 'folder/',
1190+
* limit: 100,
1191+
* })
1192+
*
1193+
* // Handle pagination
1194+
* if (data?.hasNext) {
1195+
* const nextPage = await supabase
1196+
* .storage
1197+
* .from('avatars')
1198+
* .listV2({
1199+
* prefix: 'folder/',
1200+
* cursor: data.nextCursor,
1201+
* })
1202+
* }
1203+
*
1204+
* // Handle files vs folders
1205+
* data?.objects.forEach(file => {
1206+
* if (file.id !== null) {
1207+
* console.log('File:', file.name, 'Size:', file.metadata?.size)
1208+
* }
1209+
* })
1210+
* data?.folders.forEach(folder => {
1211+
* console.log('Folder:', folder.name)
1212+
* })
1213+
* ```
11481214
*/
11491215
async listV2(
11501216
options?: SearchV2Options,

packages/core/storage-js/test/storageFileApi.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,18 @@ describe('Object API', () => {
336336
expect(res.data).toEqual([
337337
expect.objectContaining({
338338
name: uploadPath.replace('testpath/', ''),
339+
id: expect.any(String), // Files should have non-null id
340+
metadata: expect.any(Object), // Files should have metadata
339341
}),
340342
])
343+
assert(res.data)
344+
345+
// Verify files have non-null required fields
346+
const fileObj = res.data[0]
347+
expect(fileObj.id).not.toBeNull()
348+
expect(fileObj.metadata).not.toBeNull()
349+
expect(fileObj.updated_at).not.toBeNull()
350+
expect(fileObj.created_at).not.toBeNull()
341351
})
342352

343353
test('list objects V2', async () => {
@@ -520,10 +530,17 @@ describe('Object API', () => {
520530
expect(res.error).toBeNull()
521531
expect(res.data).toEqual([
522532
expect.objectContaining({
523-
bucket_id: bucketName,
524533
name: uploadPath,
534+
id: expect.any(String), // Verify it's a file, not a folder
525535
}),
526536
])
537+
assert(res.data)
538+
539+
// bucket_id may be present in remove() responses (deprecated field)
540+
// If present, verify it matches
541+
if (res.data[0].bucket_id) {
542+
expect(res.data[0].bucket_id).toBe(bucketName)
543+
}
527544
})
528545

529546
test('get object info', async () => {
@@ -545,6 +562,20 @@ describe('Object API', () => {
545562
version: expect.any(String),
546563
})
547564
)
565+
assert(res.data)
566+
567+
// Verify FileObjectV2 required fields
568+
expect(res.data.id).toBeDefined()
569+
expect(res.data.bucketId).toBeDefined()
570+
expect(res.data.lastModified).toBeDefined() // Should have this
571+
expect(res.data.size).toBeGreaterThan(0)
572+
expect(res.data.contentType).toBeDefined()
573+
expect(res.data.cacheControl).toBeDefined()
574+
expect(res.data.etag).toBeDefined()
575+
576+
// Verify updated_at does NOT exist (API returns camelCase, but the raw type shouldn't have it)
577+
// Note: The info() method uses Camelize so we check the camelCase version
578+
expect(res.data).not.toHaveProperty('updatedAt')
548579

549580
// throws when .throwOnError is enabled
550581
await expect(storage.from(bucketName).throwOnError().info('non-existent')).rejects.toThrow()

0 commit comments

Comments
 (0)