Merge pull request #330 from jonaspm/fix/upload-media-entity-overwrite

[WIP] fix: Add overwrite option to uploadToEntity method
This commit is contained in:
dswbx
2026-01-09 14:34:41 +01:00
committed by GitHub
3 changed files with 129 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
/// <reference types="@types/bun" /> /// <reference types="@types/bun" />
import { describe, expect, it } from "bun:test"; import { describe, expect, it, mock } from "bun:test";
import { Hono } from "hono"; 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 { MediaApi } from "media/api/MediaApi";
import { assetsPath, assetsTmpPath } from "../helper"; import { assetsPath, assetsTmpPath } from "../helper";
@@ -98,7 +98,7 @@ describe("MediaApi", () => {
expect(isReadableStream(res.body)).toBe(true); expect(isReadableStream(res.body)).toBe(true);
expect(isReadableStream(res.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(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0); expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png"); expect(blob.type).toBe("image/png");
@@ -113,7 +113,7 @@ describe("MediaApi", () => {
const res = await api.getFileStream(name); const res = await api.getFileStream(name);
expect(isReadableStream(res)).toBe(true); 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(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0); expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png"); expect(blob.type).toBe("image/png");
@@ -162,4 +162,30 @@ describe("MediaApi", () => {
await matches(api.upload(response.body!, { filename: "readable.png" }), "readable.png"); 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();
});
}); });

View File

@@ -8,6 +8,7 @@ import type { TAppMediaConfig } from "../../src/media/media-schema";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { assetsPath, assetsTmpPath } from "../helper"; import { assetsPath, assetsTmpPath } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import * as proto from "data/prototype";
beforeAll(() => { beforeAll(() => {
disableConsoleLog(); disableConsoleLog();
@@ -128,4 +129,87 @@ describe("MediaController", () => {
expect(destFile.exists()).resolves.toBe(true); expect(destFile.exists()).resolves.toBe(true);
await destFile.delete(); 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();
});
}); });

View File

@@ -76,6 +76,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
filename?: string; filename?: string;
path?: TInput; path?: TInput;
_init?: Omit<RequestInit, "body">; _init?: Omit<RequestInit, "body">;
query?: Record<string, any>;
}, },
): FetchPromise<ResponseObject<T>> { ): FetchPromise<ResponseObject<T>> {
const headers = { const headers = {
@@ -102,14 +103,22 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
headers, headers,
}; };
if (opts?.path) { if (opts?.path) {
return this.post(opts.path, body, init); return this.request<T>(opts.path, opts?.query, {
...init,
body,
method: "POST",
});
} }
if (!name || name.length === 0) { if (!name || name.length === 0) {
throw new Error("Invalid filename"); throw new Error("Invalid filename");
} }
return this.post<T>(opts?.path ?? ["upload", name], body, init); return this.request<T>(opts?.path ?? ["upload", name], opts?.query, {
...init,
body,
method: "POST",
});
} }
async upload<T extends FileUploadedEventData>( async upload<T extends FileUploadedEventData>(
@@ -119,6 +128,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
_init?: Omit<RequestInit, "body">; _init?: Omit<RequestInit, "body">;
path?: TInput; path?: TInput;
fetcher?: ApiFetcher; fetcher?: ApiFetcher;
query?: Record<string, any>;
} = {}, } = {},
) { ) {
if (item instanceof Request || typeof item === "string") { if (item instanceof Request || typeof item === "string") {
@@ -155,11 +165,14 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
opts?: { opts?: {
_init?: Omit<RequestInit, "body">; _init?: Omit<RequestInit, "body">;
fetcher?: typeof fetch; fetcher?: typeof fetch;
overwrite?: boolean;
}, },
): Promise<ResponseObject<FileUploadedEventData & { result: DB["media"] }>> { ): Promise<ResponseObject<FileUploadedEventData & { result: DB["media"] }>> {
const query = opts?.overwrite !== undefined ? { overwrite: opts.overwrite } : undefined;
return this.upload(item, { return this.upload(item, {
...opts, ...opts,
path: ["entity", entity, id, field], path: ["entity", entity, id, field],
query,
}); });
} }