Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .changeset/images-chainable-handle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"miniflare": patch
---

Update Images binding local mock to use chainable handle pattern

`hosted.image(imageId)` now returns a handle with `details()`, `bytes()`, `update()`, and `delete()` methods, aligning with the updated workerd API (https://github.com/cloudflare/workerd/pull/6288).
96 changes: 54 additions & 42 deletions packages/miniflare/src/workers/images/images.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Image data is stored as KV values, metadata as KV metadata
// Transforms and info operations are handled via HTTP loopback to Node.js Sharp

import { WorkerEntrypoint } from "cloudflare:workers";
import { RpcTarget, WorkerEntrypoint } from "cloudflare:workers";
import { CoreBindings, CoreHeaders } from "../core/constants";

interface Env {
Expand All @@ -29,23 +29,70 @@ async function base64DecodeStream(
return base64DecodeArrayBuffer(buffer);
}

export default class ImagesService extends WorkerEntrypoint<Env> {
async details(imageId: string): Promise<ImageMetadata | null> {
const result = await this.env.IMAGES_STORE.getWithMetadata<ImageMetadata>(
imageId,
class ImageHandleImpl extends RpcTarget {
readonly #imageId: string;
readonly #store: KVNamespace;

constructor(imageId: string, store: KVNamespace) {
super();
this.#imageId = imageId;
this.#store = store;
}

async details(): Promise<ImageMetadata | null> {
const result = await this.#store.getWithMetadata<ImageMetadata>(
this.#imageId,
"arrayBuffer"
);
return result.metadata ?? null;
}

async image(imageId: string): Promise<ReadableStream<Uint8Array> | null> {
const data = await this.env.IMAGES_STORE.get(imageId, "arrayBuffer");
async bytes(): Promise<ReadableStream<Uint8Array> | null> {
const data = await this.#store.get(this.#imageId, "arrayBuffer");
if (data === null) {
return null;
}
return new Blob([data]).stream();
}

async update(options: ImageUpdateOptions): Promise<ImageMetadata> {
const existing = await this.#store.getWithMetadata<ImageMetadata>(
this.#imageId,
"arrayBuffer"
);
if (existing.value === null || existing.metadata === null) {
throw new Error(`Image not found: ${this.#imageId}`);
}

const updatedMetadata: ImageMetadata = {
...existing.metadata,
requireSignedURLs:
options.requireSignedURLs ?? existing.metadata.requireSignedURLs,
meta: options.metadata ?? existing.metadata.meta,
creator: options.creator ?? existing.metadata.creator,
};

await this.#store.put(this.#imageId, existing.value, {
metadata: updatedMetadata,
});
return updatedMetadata;
}

async delete(): Promise<boolean> {
const existing = await this.#store.get(this.#imageId, "arrayBuffer");
if (existing === null) {
return false;
}
await this.#store.delete(this.#imageId);
return true;
}
}

export default class ImagesService extends WorkerEntrypoint<Env> {
image(imageId: string): ImageHandleImpl {
return new ImageHandleImpl(imageId, this.env.IMAGES_STORE);
}

async upload(
image: ReadableStream<Uint8Array> | ArrayBuffer,
options?: ImageUploadOptions
Expand Down Expand Up @@ -80,41 +127,6 @@ export default class ImagesService extends WorkerEntrypoint<Env> {
return metadata;
}

async update(
imageId: string,
options: ImageUpdateOptions
): Promise<ImageMetadata> {
const existing = await this.env.IMAGES_STORE.getWithMetadata<ImageMetadata>(
imageId,
"arrayBuffer"
);
if (existing.value === null || existing.metadata === null) {
throw new Error(`Image not found: ${imageId}`);
}

const updatedMetadata: ImageMetadata = {
...existing.metadata,
requireSignedURLs:
options.requireSignedURLs ?? existing.metadata.requireSignedURLs,
meta: options.metadata ?? existing.metadata.meta,
creator: options.creator ?? existing.metadata.creator,
};

await this.env.IMAGES_STORE.put(imageId, existing.value, {
metadata: updatedMetadata,
});
return updatedMetadata;
}

async delete(imageId: string): Promise<boolean> {
const existing = await this.env.IMAGES_STORE.get(imageId, "arrayBuffer");
if (existing === null) {
return false;
}
await this.env.IMAGES_STORE.delete(imageId);
return true;
}

async list(options?: ImageListOptions): Promise<ImageList> {
const limit = options?.limit ?? 50;

Expand Down
24 changes: 16 additions & 8 deletions packages/miniflare/test/plugins/images/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ describe("Images hosted CRUD", () => {

await images.hosted.upload(imageBuffer(), { id: "blob-test" });

const stream = await images.hosted.image("blob-test");
// @ts-expect-error updated types pending workerd PR #6288
const stream = await images.hosted.image("blob-test").bytes();
assert(stream !== null);
const data = new Uint8Array(await new Response(stream).arrayBuffer());
expect(data).toEqual(TEST_IMAGE_BYTES);
Expand All @@ -64,7 +65,8 @@ describe("Images hosted CRUD", () => {
);
expect(metadata.id).toBe("base64-test");

const stream = await images.hosted.image("base64-test");
// @ts-expect-error updated types pending workerd PR #6288
const stream = await images.hosted.image("base64-test").bytes();
assert(stream !== null);
const data = new Uint8Array(await new Response(stream).arrayBuffer());
expect(data).toEqual(TEST_IMAGE_BYTES);
Expand All @@ -77,7 +79,8 @@ describe("Images hosted CRUD", () => {
useDispose(mf);
const images = await mf.getImagesBinding("IMAGES");

const metadata = await images.hosted.details("does-not-exist");
// @ts-expect-error updated types pending workerd PR #6288
const metadata = await images.hosted.image("does-not-exist").details();
expect(metadata).toBe(null);
});

Expand All @@ -88,7 +91,8 @@ describe("Images hosted CRUD", () => {
useDispose(mf);
const images = await mf.getImagesBinding("IMAGES");

const stream = await images.hosted.image("does-not-exist");
// @ts-expect-error updated types pending workerd PR #6288
const stream = await images.hosted.image("does-not-exist").bytes();
expect(stream).toBe(null);
});

Expand All @@ -99,7 +103,8 @@ describe("Images hosted CRUD", () => {

await images.hosted.upload(imageBuffer(), { id: "update-test" });

const metadata = await images.hosted.update("update-test", {
// @ts-expect-error updated types pending workerd PR #6288
const metadata = await images.hosted.image("update-test").update({
requireSignedURLs: true,
});
expect(metadata.requireSignedURLs).toBe(true);
Expand All @@ -112,10 +117,12 @@ describe("Images hosted CRUD", () => {

await images.hosted.upload(imageBuffer(), { id: "delete-test" });

const deleted = await images.hosted.delete("delete-test");
// @ts-expect-error updated types pending workerd PR #6288
const deleted = await images.hosted.image("delete-test").delete();
expect(deleted).toBe(true);

const metadata = await images.hosted.details("delete-test");
// @ts-expect-error updated types pending workerd PR #6288
const metadata = await images.hosted.image("delete-test").details();
expect(metadata).toBe(null);
});

Expand All @@ -124,7 +131,8 @@ describe("Images hosted CRUD", () => {
useDispose(mf);
const images = await mf.getImagesBinding("IMAGES");

const deleted = await images.hosted.delete("does-not-exist");
// @ts-expect-error updated types pending workerd PR #6288
const deleted = await images.hosted.image("does-not-exist").delete();
expect(deleted).toBe(false);
});

Expand Down
Loading