Compare commits

..

8 Commits

Author SHA1 Message Date
dswbx 0e4f25cf11 refactor: update type handling in SvelteKit adapter tests and MediaApi uploadFile method 2026-01-09 14:51:42 +01:00
dswbx 4a5bdade8d fix: handle lazy import errors for TestRoutes in development for nextjs 2026-01-09 14:42:18 +01:00
dswbx 047bb80a97 Merge pull request #330 from jonaspm/fix/upload-media-entity-overwrite
[WIP] fix: Add overwrite option to uploadToEntity method
2026-01-09 14:34:41 +01:00
dswbx d826cb23a3 Merge pull request #291 from bknd-io/release/0.20
Release 0.20
2026-01-09 14:32:01 +01:00
dswbx f9373bd0e6 test: remove only 2026-01-09 11:30:04 +01:00
dswbx 5384738b67 tests: ensuring overwrite is passed and respected in media api 2026-01-09 11:28:19 +01:00
dswbx d82e28e6b1 Merge remote-tracking branch 'origin/release/0.20' into fork/jonaspm/fix/upload-media-entity-overwrite 2026-01-09 10:12:21 +01:00
Jonas Perusquia Morales c38bc95b1d fix: Add query support to media upload API methods 2026-01-08 12:37:13 -06:00
5 changed files with 139 additions and 13 deletions
+30 -4
View File
@@ -1,7 +1,7 @@
/// <reference types="@types/bun" />
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();
});
});
@@ -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();
});
});
@@ -9,7 +9,8 @@ afterAll(enableConsoleLog);
describe("sveltekit adapter", () => {
adapterTestSuite(bunTestRunner, {
makeApp: (c, a) => sveltekit.getApp(c, a ?? ({} as any)),
makeHandler: (c, a) => (request: Request) => sveltekit.serve(c, a ?? ({} as any))({ request }),
makeApp: (c, a) => sveltekit.getApp(c as any, a ?? ({} as any)),
makeHandler: (c, a) => (request: Request) =>
sveltekit.serve(c as any, a ?? ({} as any))({ request }),
});
});
+17 -4
View File
@@ -71,11 +71,12 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
}
protected uploadFile<T extends FileUploadedEventData>(
body: File | Blob | ReadableStream | Buffer<ArrayBufferLike>,
body: BodyInit,
opts?: {
filename?: string;
path?: TInput;
_init?: Omit<RequestInit, "body">;
query?: Record<string, any>;
},
): FetchPromise<ResponseObject<T>> {
const headers = {
@@ -102,14 +103,22 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
headers,
};
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) {
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>(
@@ -119,6 +128,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
_init?: Omit<RequestInit, "body">;
path?: TInput;
fetcher?: ApiFetcher;
query?: Record<string, any>;
} = {},
) {
if (item instanceof Request || typeof item === "string") {
@@ -144,7 +154,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
});
}
return this.uploadFile<T>(item, opts);
return this.uploadFile<T>(item as BodyInit, opts);
}
async uploadToEntity(
@@ -155,11 +165,14 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
opts?: {
_init?: Omit<RequestInit, "body">;
fetcher?: typeof fetch;
overwrite?: boolean;
},
): Promise<ResponseObject<FileUploadedEventData & { result: DB["media"] }>> {
const query = opts?.overwrite !== undefined ? { overwrite: opts.overwrite } : undefined;
return this.upload(item, {
...opts,
path: ["entity", entity, id, field],
query,
});
}
+5 -3
View File
@@ -16,9 +16,11 @@ import ToolsRoutes from "./tools";
// @ts-ignore
let TestRoutes: any;
if (import.meta.env.DEV) {
TestRoutes = lazy(() => import("./test"));
}
try {
if (import.meta.env.DEV) {
TestRoutes = lazy(() => import("./test"));
}
} catch {}
export function Routes({
BkndWrapper,