diff --git a/src/cloudflare/internal/images-api.ts b/src/cloudflare/internal/images-api.ts index 97d649a48f4..82346ddb002 100644 --- a/src/cloudflare/internal/images-api.ts +++ b/src/cloudflare/internal/images-api.ts @@ -244,11 +244,67 @@ function isDrawTransformer(input: unknown): input is DrawTransformer { return input instanceof DrawTransformer; } +interface ServiceEntrypointStub { + details(imageId: string): Promise; + image(imageId: string): Promise | null>; + upload( + image: ReadableStream | ArrayBuffer, + options?: ImageUploadOptions + ): Promise; + update(imageId: string, options: ImageUpdateOptions): Promise; + delete(imageId: string): Promise; + list(options?: ImageListOptions): Promise; +} + +class HostedImagesBindingImpl implements HostedImagesBinding { + readonly #fetcher: ServiceEntrypointStub; + + constructor(fetcher: ServiceEntrypointStub) { + this.#fetcher = fetcher; + } + + async details(imageId: string): Promise { + return this.#fetcher.details(imageId); + } + + async image(imageId: string): Promise | null> { + return this.#fetcher.image(imageId); + } + + async upload( + image: ReadableStream | ArrayBuffer, + options?: ImageUploadOptions + ): Promise { + return this.#fetcher.upload(image, options); + } + + async update( + imageId: string, + options: ImageUpdateOptions + ): Promise { + return this.#fetcher.update(imageId, options); + } + + async delete(imageId: string): Promise { + return this.#fetcher.delete(imageId); + } + + async list(options?: ImageListOptions): Promise { + return this.#fetcher.list(options || {}); + } +} + class ImagesBindingImpl implements ImagesBinding { - readonly #fetcher: Fetcher; + readonly #fetcher: Fetcher & ServiceEntrypointStub; + readonly #hosted: HostedImagesBinding; - constructor(fetcher: Fetcher) { + constructor(fetcher: Fetcher & ServiceEntrypointStub) { this.#fetcher = fetcher; + this.#hosted = new HostedImagesBindingImpl(fetcher); + } + + get hosted(): HostedImagesBinding { + return this.#hosted; } async info( @@ -356,5 +412,5 @@ async function throwErrorIfErrorResponse( } export default function makeBinding(env: { fetcher: Fetcher }): ImagesBinding { - return new ImagesBindingImpl(env.fetcher); + return new ImagesBindingImpl(env.fetcher as Fetcher & ServiceEntrypointStub); } diff --git a/src/cloudflare/internal/images.d.ts b/src/cloudflare/internal/images.d.ts index 95dccf45be2..43a0ab53cf5 100644 --- a/src/cloudflare/internal/images.d.ts +++ b/src/cloudflare/internal/images.d.ts @@ -96,6 +96,100 @@ type ImageOutputOptions = { anim?: boolean; }; +interface ImageMetadata { + id: string; + filename?: string; + uploaded?: string; + requireSignedURLs: boolean; + meta?: Record; + variants: string[]; + draft?: boolean; + creator?: string; +} + +interface ImageUploadOptions { + id?: string; + filename?: string; + requireSignedURLs?: boolean; + metadata?: Record; + creator?: string; + /** + * If 'base64', the input data will be decoded from base64 before processing + */ + encoding?: 'base64'; +} + +interface ImageUpdateOptions { + requireSignedURLs?: boolean; + metadata?: Record; + creator?: string; +} + +interface ImageListOptions { + limit?: number; + cursor?: string; + sortOrder?: 'asc' | 'desc'; + creator?: string; +} + +interface ImageList { + images: ImageMetadata[]; + cursor?: string; + listComplete: boolean; +} + +interface HostedImagesBinding { + /** + * Get metadata for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns Image metadata, or null if not found + */ + details(imageId: string): Promise; + + /** + * Get the raw image data for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns ReadableStream of image bytes, or null if not found + */ + image(imageId: string): Promise | null>; + + /** + * Upload a new hosted image + * @param image The image file to upload + * @param options Upload configuration + * @returns Metadata for the uploaded image + * @throws {@link ImagesError} if upload fails + */ + upload( + image: ReadableStream | ArrayBuffer, + options?: ImageUploadOptions + ): Promise; + + /** + * Update hosted image metadata + * @param imageId The ID of the image + * @param options Properties to update + * @returns Updated image metadata + * @throws {@link ImagesError} if update fails + */ + update(imageId: string, options: ImageUpdateOptions): Promise; + + /** + * Delete a hosted image + * @param imageId The ID of the image + * @returns True if deleted, false if not found + */ + delete(imageId: string): Promise; + + /** + * List hosted images with pagination + * @param options List configuration + * @returns List of images with pagination info + * @throws {@link ImagesError} if list fails + */ + list(options?: ImageListOptions): Promise; +} + interface ImagesBinding { /** * Get image metadata (type, width and height) @@ -115,6 +209,11 @@ interface ImagesBinding { stream: ReadableStream, options?: ImageInputOptions ): ImageTransformer; + + /** + * Access hosted images CRUD operations + */ + readonly hosted: HostedImagesBinding; } interface ImageTransformer { diff --git a/src/cloudflare/internal/test/images/BUILD.bazel b/src/cloudflare/internal/test/images/BUILD.bazel index a5d15bf1e3f..6f731c82192 100644 --- a/src/cloudflare/internal/test/images/BUILD.bazel +++ b/src/cloudflare/internal/test/images/BUILD.bazel @@ -6,5 +6,6 @@ wd_test( size = "enormous", src = "images-api-test.wd-test", args = ["--experimental"], + compat_date = "2024-04-03", data = glob(["*.js"]) + ["//src/cloudflare/internal/test:instrumentation-test-helper.js"], ) diff --git a/src/cloudflare/internal/test/images/images-api-test.js b/src/cloudflare/internal/test/images/images-api-test.js index 68e2482eaf0..0425b117b49 100644 --- a/src/cloudflare/internal/test/images/images-api-test.js +++ b/src/cloudflare/internal/test/images/images-api-test.js @@ -451,3 +451,238 @@ export const test_images_base64_small_chunks = { ); }, }; +// GET metadata +export const test_images_get_success = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const metadata = await env.images.hosted.details('test-image-id'); + assert.notEqual(metadata, null); + assert.equal(metadata.id, 'test-image-id'); + assert.equal(metadata.filename, 'test.jpg'); + assert.equal(metadata.requireSignedURLs, false); + assert.equal(metadata.creator, 'test-creator'); + }, +}; + +export const test_images_get_not_found = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const metadata = await env.images.hosted.details('not-found'); + assert.equal(metadata, null); + }, +}; + +// GET image blob +export const test_images_getImage_success = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const stream = await env.images.hosted.image('test-image-id'); + assert.notEqual(stream, null); + + const reader = stream.getReader(); + let result = ''; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + result += new TextDecoder().decode(value); + } + + assert.equal(result, 'MOCK_IMAGE_DATA_test-image-id'); + }, +}; + +export const test_images_getImage_not_found = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const stream = await env.images.hosted.image('not-found'); + assert.equal(stream, null); + }, +}; + +// UPLOAD +export const test_images_upload_with_options = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const imageData = new Blob(['test image']).stream(); + const metadata = await env.images.hosted.upload(imageData, { + id: 'custom-id', + filename: 'upload-test.jpg', + requireSignedURLs: true, + metadata: { key: 'value' }, + creator: 'upload-creator', + }); + + assert.equal(metadata.id, 'custom-id'); + assert.equal(metadata.filename, 'upload-test.jpg'); + assert.equal(metadata.requireSignedURLs, true); + assert.deepStrictEqual(metadata.meta, { key: 'value' }); + assert.equal(metadata.creator, 'upload-creator'); + }, +}; + +export const test_images_upload_arraybuffer = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const buffer = new TextEncoder().encode('test image').buffer; + const metadata = await env.images.hosted.upload(buffer); + + assert.notEqual(metadata, null); + assert.equal(typeof metadata.id, 'string'); + }, +}; + +// UPDATE +export const test_images_update_success = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const metadata = await env.images.hosted.update('test-image-id', { + requireSignedURLs: true, + metadata: { updated: true }, + creator: 'update-creator', + }); + + assert.equal(metadata.id, 'test-image-id'); + assert.equal(metadata.requireSignedURLs, true); + assert.deepStrictEqual(metadata.meta, { updated: true }); + assert.equal(metadata.creator, 'update-creator'); + }, +}; + +export const test_images_update_not_found = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + /** + * @type {any} e; + */ + let e; + try { + await env.images.hosted.update('not-found', { requireSignedURLs: true }); + } catch (err) { + e = err; + } + assert.notEqual(e, undefined); + assert.equal(e.message.includes('not found'), true); + }, +}; + +// DELETE +export const test_images_delete_success = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const result = await env.images.hosted.delete('test-image-id'); + assert.equal(result, true); + }, +}; + +export const test_images_delete_not_found = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const result = await env.images.hosted.delete('not-found'); + assert.equal(result, false); + }, +}; + +// LIST +export const test_images_list_default = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const result = await env.images.hosted.list(); + + assert.notEqual(result.images, null); + assert.equal(Array.isArray(result.images), true); + assert.equal(result.images.length, 2); + assert.equal(result.listComplete, true); + }, +}; + +export const test_images_list_with_options = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + const result = await env.images.hosted.list({ + limit: 1, + sortOrder: 'asc', + }); + + assert.equal(result.images.length, 1); + // TODO(IMAGES-2032): Test cursor once pagination is implemented + }, +}; + +// UPLOAD with base64 encoding +export const test_images_upload_base64_stream = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + // Create base64-encoded data + const imageData = 'test image content'; + const base64Data = btoa(imageData); + const stream = new Blob([base64Data]).stream(); + + const metadata = await env.images.hosted.upload(stream, { + filename: 'base64-test.jpg', + encoding: 'base64', + }); + + assert.equal(metadata.filename, 'base64-test.jpg'); + assert.notEqual(metadata.id, null); + }, +}; + +export const test_images_upload_base64_arraybuffer = { + /** + * @param {unknown} _ + * @param {Env} env + */ + async test(_, env) { + // Create base64-encoded ArrayBuffer + const imageData = 'test image content'; + const base64Data = btoa(imageData); + const buffer = new TextEncoder().encode(base64Data).buffer; + + const metadata = await env.images.hosted.upload(buffer, { + filename: 'base64-buffer-test.jpg', + encoding: 'base64', + }); + + assert.equal(metadata.filename, 'base64-buffer-test.jpg'); + assert.notEqual(metadata.id, null); + }, +}; diff --git a/src/cloudflare/internal/test/images/images-api-test.wd-test b/src/cloudflare/internal/test/images/images-api-test.wd-test index 8a807d80368..190874749a6 100644 --- a/src/cloudflare/internal/test/images/images-api-test.wd-test +++ b/src/cloudflare/internal/test/images/images-api-test.wd-test @@ -7,16 +7,21 @@ const unitTests :Workerd.Config = ( modules = [ ( name = "worker", esModule = embed "images-api-test.js" ) ], - compatibilityFlags = ["experimental", "nodejs_compat", "nodejs_compat_v2", "service_binding_extra_handlers", "streams_enable_constructors", "transformstream_enable_standard_constructor", "formdata_parser_supports_files"], + compatibilityFlags = ["experimental", "nodejs_compat", "nodejs_compat_v2", "service_binding_extra_handlers", "rpc", "streams_enable_constructors", "transformstream_enable_standard_constructor", "formdata_parser_supports_files"], streamingTails = ["tail"], bindings = [ ( name = "images", wrapped = ( moduleName = "cloudflare-internal:images-api", - innerBindings = [( - name = "fetcher", - service = "images-upstream-mock" - )], + innerBindings = [ + ( + name = "fetcher", + service = ( + name = "images-upstream-mock", + entrypoint = "ServiceEntrypoint" + ) + ) + ], ) ) ], diff --git a/src/cloudflare/internal/test/images/images-upstream-mock.js b/src/cloudflare/internal/test/images/images-upstream-mock.js index 53ebb115260..31b3ed69fa4 100644 --- a/src/cloudflare/internal/test/images/images-upstream-mock.js +++ b/src/cloudflare/internal/test/images/images-upstream-mock.js @@ -1,7 +1,9 @@ -// Copyright (c) 2024 Cloudflare, Inc. +// Copyright (c) 2025 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 +import { WorkerEntrypoint } from 'cloudflare:workers'; + /** * @param {FormDataEntryValue | null} blob * @returns {Promise} @@ -18,21 +20,146 @@ async function imageAsString(blob) { return blob.text(); } -export default { +export class ServiceEntrypoint extends WorkerEntrypoint { /** + * @param {string} imageId + * @returns {Promise} + */ + async details(imageId) { + if (imageId === 'not-found') { + return null; + } + + return { + id: imageId, + filename: 'test.jpg', + uploaded: '2024-01-01T00:00:00Z', + requireSignedURLs: false, + variants: ['public'], + meta: {}, + draft: false, + creator: 'test-creator', + }; + } + + async image(imageId) { + if (imageId === 'not-found') { + return null; + } + + const mockData = `MOCK_IMAGE_DATA_${imageId}`; + return new Blob([mockData]).stream(); + } + + async upload(image, options) { + // Handle both ReadableStream and ArrayBuffer + const buffer = + image instanceof ArrayBuffer + ? image + : await new Response(image).arrayBuffer(); + + const decoder = new TextDecoder(); + const text = decoder.decode(buffer); + + if (text === 'INVALID') { + throw new Error('Invalid image data'); + } + + return { + id: options?.id || 'generated-id', + filename: options?.filename || 'uploaded.jpg', + uploaded: '2024-01-01T00:00:00Z', + requireSignedURLs: options?.requireSignedURLs || false, + variants: ['public'], + meta: options?.metadata || {}, + draft: false, + creator: options?.creator, + }; + } + + /** + * @param {string} imageId + * @param {ImageUpdateOptions} body + * @returns {Promise} + */ + async update(imageId, body) { + if (imageId === 'not-found') { + throw new Error('Image not found'); + } + + return { + id: imageId, + filename: 'updated.jpg', + uploaded: '2024-01-01T00:00:00Z', + requireSignedURLs: + body.requireSignedURLs !== undefined ? body.requireSignedURLs : false, + variants: ['public'], + meta: body.metadata || {}, + draft: false, + creator: body.creator, + }; + } + + /** + * @param {string} imageId + * @returns {Promise} + */ + async delete(imageId) { + return imageId !== 'not-found'; + } + + /** + * @param {ImageListOptions} [options] + * @returns {Promise} + */ + async list(options) { + const images = [ + { + id: 'image-1', + filename: 'test1.jpg', + uploaded: '2024-01-01T00:00:00Z', + requireSignedURLs: false, + variants: ['public'], + meta: {}, + creator: 'test-creator', + }, + { + id: 'image-2', + filename: 'test2.jpg', + uploaded: '2024-01-02T00:00:00Z', + requireSignedURLs: false, + variants: ['public'], + meta: {}, + creator: 'test-creator', + }, + ]; + + const limit = options?.limit || 50; + const slicedImages = images.slice(0, limit); + + return { + images: slicedImages, + listComplete: true, + }; + } + + /** + * Handle HTTP requests for info and transform operations. + * In production these go to a separate transformation service, + * but in tests we mock both the ServiceEntrypoint and transformation service in one place. * @param {Request} request + * @returns {Promise} */ async fetch(request) { const form = await request.formData(); const image = (await imageAsString(form.get('image'))) || ''; if (image.includes('BAD')) { - const resp = new Response('ERROR 123: Bad request', { + return new Response('ERROR 123: Bad request', { status: 409, headers: { 'cf-images-binding': 'err=123', }, }); - return resp; } switch (new URL(request.url).pathname) { @@ -49,10 +176,8 @@ export default { height: 123, }); } - case '/transform': - /** - * @type {any} - */ + case '/transform': { + /** @type {any} */ const obj = { image: await imageAsString(form.get('image')), // @ts-ignore @@ -78,8 +203,22 @@ export default { } return Response.json(obj); + } } throw new Error('Unexpected mock invocation'); + } +} + +export default { + /** + * @param {Request} request + * @param {*} env + * @param {ExecutionContext} ctx + * @returns {Promise} + */ + async fetch(request, env, ctx) { + const entrypoint = new ServiceEntrypoint(ctx, env); + return entrypoint.fetch(request); }, }; diff --git a/types/defines/images.d.ts b/types/defines/images.d.ts index 95dccf45be2..984cd6cea7f 100644 --- a/types/defines/images.d.ts +++ b/types/defines/images.d.ts @@ -96,6 +96,97 @@ type ImageOutputOptions = { anim?: boolean; }; +interface ImageMetadata { + id: string; + filename?: string; + uploaded?: string; + requireSignedURLs: boolean; + meta?: Record; + variants: string[]; + draft?: boolean; + creator?: string; +} + +interface ImageUploadOptions { + id?: string; + filename?: string; + requireSignedURLs?: boolean; + metadata?: Record; + creator?: string; + encoding?: 'base64'; +} + +interface ImageUpdateOptions { + requireSignedURLs?: boolean; + metadata?: Record; + creator?: string; +} + +interface ImageListOptions { + limit?: number; + cursor?: string; + sortOrder?: 'asc' | 'desc'; + creator?: string; +} + +interface ImageList { + images: ImageMetadata[]; + cursor?: string; + listComplete: boolean; +} + +interface HostedImagesBinding { + /** + * Get detailed metadata for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns Image metadata, or null if not found + */ + details(imageId: string): Promise; + + /** + * Get the raw image data for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns ReadableStream of image bytes, or null if not found + */ + image(imageId: string): Promise | null>; + + /** + * Upload a new hosted image + * @param image The image file to upload + * @param options Upload configuration + * @returns Metadata for the uploaded image + * @throws {@link ImagesError} if upload fails + */ + upload( + image: ReadableStream | ArrayBuffer, + options?: ImageUploadOptions + ): Promise; + + /** + * Update hosted image metadata + * @param imageId The ID of the image + * @param options Properties to update + * @returns Updated image metadata + * @throws {@link ImagesError} if update fails + */ + update(imageId: string, options: ImageUpdateOptions): Promise; + + /** + * Delete a hosted image + * @param imageId The ID of the image + * @returns True if deleted, false if not found + */ + delete(imageId: string): Promise; + + /** + * List hosted images with pagination + * @param options List configuration + * @returns List of images with pagination info + * @throws {@link ImagesError} if list fails + */ + list(options?: ImageListOptions): Promise; +} + interface ImagesBinding { /** * Get image metadata (type, width and height) @@ -115,6 +206,11 @@ interface ImagesBinding { stream: ReadableStream, options?: ImageInputOptions ): ImageTransformer; + + /** + * Access hosted images CRUD operations + */ + readonly hosted: HostedImagesBinding; } interface ImageTransformer { diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index d09a886efaf..19db907984e 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -11605,6 +11605,38 @@ type ImageOutputOptions = { background?: string; anim?: boolean; }; +interface ImageMetadata { + id: string; + filename?: string; + uploaded?: string; + requireSignedURLs: boolean; + meta?: Record; + variants: string[]; + draft?: boolean; + creator?: string; +} +interface ImageUploadOptions { + id?: string; + filename?: string; + requireSignedURLs?: boolean; + metadata?: Record; + encoding?: "base64"; +} +interface ImageUpdateOptions { + requireSignedURLs?: boolean; + metadata?: Record; +} +interface ImageListOptions { + limit?: number; + cursor?: string; + sortOrder?: "asc" | "desc"; + creator?: string; +} +interface ImageList { + images: ImageMetadata[]; + cursor?: string; + listComplete: boolean; +} interface ImagesBinding { /** * Get image metadata (type, width and height) @@ -11624,6 +11656,50 @@ interface ImagesBinding { stream: ReadableStream, options?: ImageInputOptions, ): ImageTransformer; + /** + * Get metadata for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns Image metadata, or null if not found + */ + get(imageId: string): Promise; + /** + * Get the raw image data for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns ReadableStream of image bytes, or null if not found + */ + getImage(imageId: string): Promise | null>; + /** + * Upload a new hosted image + * @param image The image file to upload + * @param options Upload configuration + * @returns Metadata for the uploaded image + * @throws {@link ImagesError} if upload fails + */ + upload( + image: ReadableStream | ArrayBuffer, + options?: ImageUploadOptions, + ): Promise; + /** + * Update hosted image metadata + * @param imageId The ID of the image + * @param options Properties to update + * @returns Updated image metadata + * @throws {@link ImagesError} if update fails + */ + update(imageId: string, options: ImageUpdateOptions): Promise; + /** + * Delete a hosted image + * @param imageId The ID of the image + * @returns True if deleted, false if not found + */ + delete(imageId: string): Promise; + /** + * List hosted images with pagination + * @param options List configuration + * @returns List of images with pagination info + * @throws {@link ImagesError} if list fails + */ + list(options?: ImageListOptions): Promise; } interface ImageTransformer { /** diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index 220683f1315..532e7c7a5ae 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -11622,6 +11622,38 @@ export type ImageOutputOptions = { background?: string; anim?: boolean; }; +export interface ImageMetadata { + id: string; + filename?: string; + uploaded?: string; + requireSignedURLs: boolean; + meta?: Record; + variants: string[]; + draft?: boolean; + creator?: string; +} +export interface ImageUploadOptions { + id?: string; + filename?: string; + requireSignedURLs?: boolean; + metadata?: Record; + encoding?: "base64"; +} +export interface ImageUpdateOptions { + requireSignedURLs?: boolean; + metadata?: Record; +} +export interface ImageListOptions { + limit?: number; + cursor?: string; + sortOrder?: "asc" | "desc"; + creator?: string; +} +export interface ImageList { + images: ImageMetadata[]; + cursor?: string; + listComplete: boolean; +} export interface ImagesBinding { /** * Get image metadata (type, width and height) @@ -11641,6 +11673,50 @@ export interface ImagesBinding { stream: ReadableStream, options?: ImageInputOptions, ): ImageTransformer; + /** + * Get metadata for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns Image metadata, or null if not found + */ + get(imageId: string): Promise; + /** + * Get the raw image data for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns ReadableStream of image bytes, or null if not found + */ + getImage(imageId: string): Promise | null>; + /** + * Upload a new hosted image + * @param image The image file to upload + * @param options Upload configuration + * @returns Metadata for the uploaded image + * @throws {@link ImagesError} if upload fails + */ + upload( + image: ReadableStream | ArrayBuffer, + options?: ImageUploadOptions, + ): Promise; + /** + * Update hosted image metadata + * @param imageId The ID of the image + * @param options Properties to update + * @returns Updated image metadata + * @throws {@link ImagesError} if update fails + */ + update(imageId: string, options: ImageUpdateOptions): Promise; + /** + * Delete a hosted image + * @param imageId The ID of the image + * @returns True if deleted, false if not found + */ + delete(imageId: string): Promise; + /** + * List hosted images with pagination + * @param options List configuration + * @returns List of images with pagination info + * @throws {@link ImagesError} if list fails + */ + list(options?: ImageListOptions): Promise; } export interface ImageTransformer { /** diff --git a/types/generated-snapshot/latest/index.d.ts b/types/generated-snapshot/latest/index.d.ts index 6aa7a7a4222..8568275931d 100755 --- a/types/generated-snapshot/latest/index.d.ts +++ b/types/generated-snapshot/latest/index.d.ts @@ -11001,6 +11001,38 @@ type ImageOutputOptions = { background?: string; anim?: boolean; }; +interface ImageMetadata { + id: string; + filename?: string; + uploaded?: string; + requireSignedURLs: boolean; + meta?: Record; + variants: string[]; + draft?: boolean; + creator?: string; +} +interface ImageUploadOptions { + id?: string; + filename?: string; + requireSignedURLs?: boolean; + metadata?: Record; + encoding?: "base64"; +} +interface ImageUpdateOptions { + requireSignedURLs?: boolean; + metadata?: Record; +} +interface ImageListOptions { + limit?: number; + cursor?: string; + sortOrder?: "asc" | "desc"; + creator?: string; +} +interface ImageList { + images: ImageMetadata[]; + cursor?: string; + listComplete: boolean; +} interface ImagesBinding { /** * Get image metadata (type, width and height) @@ -11020,6 +11052,50 @@ interface ImagesBinding { stream: ReadableStream, options?: ImageInputOptions, ): ImageTransformer; + /** + * Get metadata for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns Image metadata, or null if not found + */ + get(imageId: string): Promise; + /** + * Get the raw image data for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns ReadableStream of image bytes, or null if not found + */ + getImage(imageId: string): Promise | null>; + /** + * Upload a new hosted image + * @param image The image file to upload + * @param options Upload configuration + * @returns Metadata for the uploaded image + * @throws {@link ImagesError} if upload fails + */ + upload( + image: ReadableStream | ArrayBuffer, + options?: ImageUploadOptions, + ): Promise; + /** + * Update hosted image metadata + * @param imageId The ID of the image + * @param options Properties to update + * @returns Updated image metadata + * @throws {@link ImagesError} if update fails + */ + update(imageId: string, options: ImageUpdateOptions): Promise; + /** + * Delete a hosted image + * @param imageId The ID of the image + * @returns True if deleted, false if not found + */ + delete(imageId: string): Promise; + /** + * List hosted images with pagination + * @param options List configuration + * @returns List of images with pagination info + * @throws {@link ImagesError} if list fails + */ + list(options?: ImageListOptions): Promise; } interface ImageTransformer { /** diff --git a/types/generated-snapshot/latest/index.ts b/types/generated-snapshot/latest/index.ts index e12a44e6d6d..4973546daa4 100755 --- a/types/generated-snapshot/latest/index.ts +++ b/types/generated-snapshot/latest/index.ts @@ -11018,6 +11018,38 @@ export type ImageOutputOptions = { background?: string; anim?: boolean; }; +export interface ImageMetadata { + id: string; + filename?: string; + uploaded?: string; + requireSignedURLs: boolean; + meta?: Record; + variants: string[]; + draft?: boolean; + creator?: string; +} +export interface ImageUploadOptions { + id?: string; + filename?: string; + requireSignedURLs?: boolean; + metadata?: Record; + encoding?: "base64"; +} +export interface ImageUpdateOptions { + requireSignedURLs?: boolean; + metadata?: Record; +} +export interface ImageListOptions { + limit?: number; + cursor?: string; + sortOrder?: "asc" | "desc"; + creator?: string; +} +export interface ImageList { + images: ImageMetadata[]; + cursor?: string; + listComplete: boolean; +} export interface ImagesBinding { /** * Get image metadata (type, width and height) @@ -11037,6 +11069,50 @@ export interface ImagesBinding { stream: ReadableStream, options?: ImageInputOptions, ): ImageTransformer; + /** + * Get metadata for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns Image metadata, or null if not found + */ + get(imageId: string): Promise; + /** + * Get the raw image data for a hosted image + * @param imageId The ID of the image (UUID or custom ID) + * @returns ReadableStream of image bytes, or null if not found + */ + getImage(imageId: string): Promise | null>; + /** + * Upload a new hosted image + * @param image The image file to upload + * @param options Upload configuration + * @returns Metadata for the uploaded image + * @throws {@link ImagesError} if upload fails + */ + upload( + image: ReadableStream | ArrayBuffer, + options?: ImageUploadOptions, + ): Promise; + /** + * Update hosted image metadata + * @param imageId The ID of the image + * @param options Properties to update + * @returns Updated image metadata + * @throws {@link ImagesError} if update fails + */ + update(imageId: string, options: ImageUpdateOptions): Promise; + /** + * Delete a hosted image + * @param imageId The ID of the image + * @returns True if deleted, false if not found + */ + delete(imageId: string): Promise; + /** + * List hosted images with pagination + * @param options List configuration + * @returns List of images with pagination info + * @throws {@link ImagesError} if list fails + */ + list(options?: ImageListOptions): Promise; } export interface ImageTransformer { /**