diff --git a/app/__test__/api/MediaApi.spec.ts b/app/__test__/api/MediaApi.spec.ts
index 02a35a6..ee1dcb3 100644
--- a/app/__test__/api/MediaApi.spec.ts
+++ b/app/__test__/api/MediaApi.spec.ts
@@ -1,7 +1,7 @@
///
-import { describe, expect, it } from "bun:test";
+import { describe, expect, it, mock } from "bun:test";
import { Hono } from "hono";
-import { getFileFromContext, isFile, isReadableStream } from "core/utils";
+import { getFileFromContext, isFile, isReadableStream, s, jsc } from "core/utils";
import { MediaApi } from "media/api/MediaApi";
import { assetsPath, assetsTmpPath } from "../helper";
@@ -98,7 +98,7 @@ describe("MediaApi", () => {
expect(isReadableStream(res.body)).toBe(true);
expect(isReadableStream(res.res.body)).toBe(true);
- const blob = await res.res.blob();
+ const blob = (await res.res.blob()) as File;
expect(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png");
@@ -113,7 +113,7 @@ describe("MediaApi", () => {
const res = await api.getFileStream(name);
expect(isReadableStream(res)).toBe(true);
- const blob = await new Response(res).blob();
+ const blob = (await new Response(res).blob()) as File;
expect(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png");
@@ -162,4 +162,30 @@ describe("MediaApi", () => {
await matches(api.upload(response.body!, { filename: "readable.png" }), "readable.png");
}
});
+
+ it("should add overwrite query for entity upload", async (c) => {
+ const call = mock(() => null);
+ const hono = new Hono().post(
+ "/api/media/entity/:entity/:id/:field",
+ jsc("query", s.object({ overwrite: s.boolean().optional() })),
+ async (c) => {
+ const { overwrite } = c.req.valid("query");
+ expect(overwrite).toBe(true);
+ call();
+ return c.json({ ok: true });
+ },
+ );
+ const api = new MediaApi(
+ {
+ upload_fetcher: hono.request,
+ },
+ hono.request,
+ );
+ const file = Bun.file(`${assetsPath}/image.png`);
+ const res = await api.uploadToEntity("posts", 1, "cover", file as any, {
+ overwrite: true,
+ });
+ expect(res.ok).toBe(true);
+ expect(call).toHaveBeenCalled();
+ });
});
diff --git a/app/__test__/media/MediaController.spec.ts b/app/__test__/media/MediaController.spec.ts
index 3eae83e..39da5a4 100644
--- a/app/__test__/media/MediaController.spec.ts
+++ b/app/__test__/media/MediaController.spec.ts
@@ -8,6 +8,7 @@ import type { TAppMediaConfig } from "../../src/media/media-schema";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { assetsPath, assetsTmpPath } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
+import * as proto from "data/prototype";
beforeAll(() => {
disableConsoleLog();
@@ -128,4 +129,87 @@ describe("MediaController", () => {
expect(destFile.exists()).resolves.toBe(true);
await destFile.delete();
});
+
+ test("entity upload with max_items and overwrite", async () => {
+ const app = createApp({
+ config: {
+ media: mergeObject(
+ {
+ enabled: true,
+ adapter: {
+ type: "local",
+ config: {
+ path: assetsTmpPath,
+ },
+ },
+ },
+ {},
+ ),
+ data: {
+ entities: {
+ posts: proto
+ .entity("posts", {
+ title: proto.text(),
+ cover: proto.medium(),
+ })
+ .toJSON(),
+ },
+ },
+ },
+ });
+ await app.build();
+
+ // create a post first
+ const createRes = await app.server.request("/api/data/entity/posts", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ title: "Test Post" }),
+ });
+ expect(createRes.status).toBe(201);
+ const { data: post } = (await createRes.json()) as any;
+
+ const file = Bun.file(path);
+ const uploadedFiles: string[] = [];
+
+ // upload first file to entity (should succeed)
+ const res1 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
+ method: "POST",
+ body: file,
+ });
+ expect(res1.status).toBe(201);
+ const result1 = (await res1.json()) as any;
+ uploadedFiles.push(result1.name);
+
+ // upload second file without overwrite (should fail - max_items reached)
+ const res2 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
+ method: "POST",
+ body: file,
+ });
+ expect(res2.status).toBe(400);
+ const result2 = (await res2.json()) as any;
+ expect(result2.error).toContain("Max items");
+
+ // upload third file with overwrite=true (should succeed and delete old file)
+ const res3 = await app.server.request(
+ `/api/media/entity/posts/${post.id}/cover?overwrite=true`,
+ {
+ method: "POST",
+ body: file,
+ },
+ );
+ expect(res3.status).toBe(201);
+ const result3 = (await res3.json()) as any;
+ uploadedFiles.push(result3.name);
+
+ // verify old file was deleted from storage
+ const oldFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[0]);
+ expect(await oldFile.exists()).toBe(false);
+
+ // verify new file exists
+ const newFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[1]);
+ expect(await newFile.exists()).toBe(true);
+
+ // cleanup
+ await newFile.delete();
+ });
});
diff --git a/app/src/media/api/MediaApi.ts b/app/src/media/api/MediaApi.ts
index d925d4d..591d0eb 100644
--- a/app/src/media/api/MediaApi.ts
+++ b/app/src/media/api/MediaApi.ts
@@ -76,6 +76,7 @@ export class MediaApi extends ModuleApi {
filename?: string;
path?: TInput;
_init?: Omit;
+ query?: Record;
},
): FetchPromise> {
const headers = {
@@ -102,14 +103,22 @@ export class MediaApi extends ModuleApi {
headers,
};
if (opts?.path) {
- return this.post(opts.path, body, init);
+ return this.request(opts.path, opts?.query, {
+ ...init,
+ body,
+ method: "POST",
+ });
}
if (!name || name.length === 0) {
throw new Error("Invalid filename");
}
- return this.post(opts?.path ?? ["upload", name], body, init);
+ return this.request(opts?.path ?? ["upload", name], opts?.query, {
+ ...init,
+ body,
+ method: "POST",
+ });
}
async upload(
@@ -119,6 +128,7 @@ export class MediaApi extends ModuleApi {
_init?: Omit;
path?: TInput;
fetcher?: ApiFetcher;
+ query?: Record;
} = {},
) {
if (item instanceof Request || typeof item === "string") {
@@ -155,11 +165,14 @@ export class MediaApi extends ModuleApi {
opts?: {
_init?: Omit;
fetcher?: typeof fetch;
+ overwrite?: boolean;
},
): Promise> {
+ const query = opts?.overwrite !== undefined ? { overwrite: opts.overwrite } : undefined;
return this.upload(item, {
...opts,
path: ["entity", entity, id, field],
+ query,
});
}