diff --git a/app/__test__/api/MediaApi.spec.ts b/app/__test__/api/MediaApi.spec.ts index 7387792..27e86da 100644 --- a/app/__test__/api/MediaApi.spec.ts +++ b/app/__test__/api/MediaApi.spec.ts @@ -117,27 +117,30 @@ describe("MediaApi", () => { const url = "http://localhost/api/media/file/image.png"; // upload bun file - await matches(api.upload(file as any, "bunfile.png"), "bunfile.png"); + await matches(api.upload(file as any, { filename: "bunfile.png" }), "bunfile.png"); // upload via request - await matches(api.upload(new Request(url), "request.png"), "request.png"); + await matches(api.upload(new Request(url), { filename: "request.png" }), "request.png"); // upload via url - await matches(api.upload(url, "url.png"), "url.png"); + await matches(api.upload(url, { filename: "url.png" }), "url.png"); // upload via response { const response = await mockedBackend.request(url); - await matches(api.upload(response, "response.png"), "response.png"); + await matches(api.upload(response, { filename: "response.png" }), "response.png"); } // upload via readable from bun - await matches(await api.upload(file.stream(), "readable.png"), "readable.png"); + await matches(await api.upload(file.stream(), { filename: "readable.png" }), "readable.png"); // upload via readable from response { const response = (await mockedBackend.request(url)) as Response; - await matches(await api.upload(response.body!, "readable.png"), "readable.png"); + await matches( + await api.upload(response.body!, { filename: "readable.png" }), + "readable.png" + ); } }); }); diff --git a/app/package.json b/app/package.json index 1952b4d..0e66341 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.8.0-rc.3", + "version": "0.8.0-rc.4", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { diff --git a/app/src/core/events/EventListener.ts b/app/src/core/events/EventListener.ts index fc677ed..d3ac6ff 100644 --- a/app/src/core/events/EventListener.ts +++ b/app/src/core/events/EventListener.ts @@ -14,10 +14,17 @@ export class EventListener { event: EventClass; handler: ListenerHandler; once: boolean = false; + id?: string; - constructor(event: EventClass, handler: ListenerHandler, mode: ListenerMode = "async") { + constructor( + event: EventClass, + handler: ListenerHandler, + mode: ListenerMode = "async", + id?: string + ) { this.event = event; this.handler = handler; this.mode = mode; + this.id = id; } } diff --git a/app/src/core/events/EventManager.ts b/app/src/core/events/EventManager.ts index 73764ea..a772362 100644 --- a/app/src/core/events/EventManager.ts +++ b/app/src/core/events/EventManager.ts @@ -6,6 +6,7 @@ export type RegisterListenerConfig = | { mode?: ListenerMode; once?: boolean; + id?: string; }; export interface EmitsEvents { @@ -124,6 +125,14 @@ export class EventManager< addListener(listener: EventListener) { this.throwIfEventNotRegistered(listener.event); + if (listener.id) { + const existing = this.listeners.find((l) => l.id === listener.id); + if (existing) { + console.warn(`Listener with id "${listener.id}" already exists.`); + return this; + } + } + this.listeners.push(listener); return this; } @@ -140,6 +149,9 @@ export class EventManager< if (config.once) { listener.once = true; } + if (config.id) { + listener.id = `${event.slug}-${config.id}`; + } this.addListener(listener as any); } diff --git a/app/src/core/utils/reqres.ts b/app/src/core/utils/reqres.ts index e8fa4d4..ea29a80 100644 --- a/app/src/core/utils/reqres.ts +++ b/app/src/core/utils/reqres.ts @@ -207,7 +207,7 @@ export async function blobToFile( if (isFile(blob)) return blob; if (!isBlob(blob)) throw new Error("Not a Blob"); - const type = !isMimeType(overrides.type, ["application/octet-stream"]) + const type = isMimeType(overrides.type, ["application/octet-stream"]) ? overrides.type : await detectMimeType(blob); const ext = type ? extension(type) : ""; diff --git a/app/src/data/entities/Mutator.ts b/app/src/data/entities/Mutator.ts index 15760bc..198cb11 100644 --- a/app/src/data/entities/Mutator.ts +++ b/app/src/data/entities/Mutator.ts @@ -107,18 +107,24 @@ export class Mutator< protected async many(qb: MutatorQB): Promise { const entity = this.entity; const { sql, parameters } = qb.compile(); - //console.log("mutatoar:exec", sql, parameters); - const result = await qb.execute(); - const data = this.em.hydrate(entity.name, result) as EntityData[]; + try { + const result = await qb.execute(); - return { - entity, - sql, - parameters: [...parameters], - result: result, - data - }; + const data = this.em.hydrate(entity.name, result) as EntityData[]; + + return { + entity, + sql, + parameters: [...parameters], + result: result, + data + }; + } catch (e) { + // @todo: redact + console.log("[Error in query]", sql); + throw e; + } } protected async single(qb: MutatorQB): Promise> { diff --git a/app/src/media/AppMedia.ts b/app/src/media/AppMedia.ts index 4924ed9..b2040ce 100644 --- a/app/src/media/AppMedia.ts +++ b/app/src/media/AppMedia.ts @@ -126,9 +126,8 @@ export class AppMedia extends Module { const payload = this.uploadedEventDataToMediaPayload(e.params); await mutator.insertOne(payload); mutator.__unstable_toggleSystemEntityCreation(true); - console.log("App:storage:file uploaded", e); }, - "sync" + { mode: "sync", id: "add-data-media" } ); // when file is deleted, sync with media entity @@ -144,7 +143,7 @@ export class AppMedia extends Module { console.log("App:storage:file deleted", e); }, - "sync" + { mode: "sync", id: "delete-data-media" } ); } diff --git a/app/src/media/api/MediaApi.ts b/app/src/media/api/MediaApi.ts index 83dd504..bf3277b 100644 --- a/app/src/media/api/MediaApi.ts +++ b/app/src/media/api/MediaApi.ts @@ -1,5 +1,10 @@ import type { FileListObject } from "media"; -import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules/ModuleApi"; +import { + type BaseModuleApiOptions, + ModuleApi, + type PrimaryFieldType, + type TInput +} from "modules/ModuleApi"; import type { FileWithPath } from "ui/elements/media/file-selector"; export type MediaApiOptions = BaseModuleApiOptions & {}; @@ -53,12 +58,24 @@ export class MediaApi extends ModuleApi { }); } - protected uploadFile(body: File | ReadableStream, filename?: string) { - let type: string = "application/octet-stream"; - let name: string = filename || ""; + protected uploadFile( + body: File | ReadableStream, + opts?: { + filename?: string; + path?: TInput; + _init?: Omit; + } + ) { + const headers = { + "Content-Type": "application/octet-stream", + ...(opts?._init?.headers || {}) + }; + let name: string = opts?.filename || ""; try { - type = (body as File).type; - if (!filename) { + if (typeof (body as File).type !== "undefined") { + headers["Content-Type"] = (body as File).type; + } + if (!opts?.filename) { name = (body as File).name; } } catch (e) {} @@ -67,32 +84,67 @@ export class MediaApi extends ModuleApi { name = name.split("/").pop() || ""; } + const init = { + ...(opts?._init || {}), + headers + }; + if (opts?.path) { + return this.post(opts.path, body, init); + } + if (!name || name.length === 0) { throw new Error("Invalid filename"); } - return this.post(["upload", name], body, { - headers: { - "Content-Type": type - } - }); + return this.post(opts?.path ?? ["upload", name], body, init); } - async upload(item: Request | Response | string | File | ReadableStream, filename?: string) { + async upload( + item: Request | Response | string | File | ReadableStream, + opts: { + filename?: string; + _init?: Omit; + path?: TInput; + } = {} + ) { if (item instanceof Request || typeof item === "string") { const res = await this.fetcher(item); if (!res.ok || !res.body) { throw new Error("Failed to fetch file"); } - return this.uploadFile(res.body, filename); + return this.uploadFile(res.body, opts); } else if (item instanceof Response) { if (!item.body) { throw new Error("Invalid response"); } - return this.uploadFile(item.body, filename); + return this.uploadFile(item.body, { + ...(opts ?? {}), + _init: { + ...(opts._init ?? {}), + headers: { + ...(opts._init?.headers ?? {}), + "Content-Type": item.headers.get("Content-Type") || "application/octet-stream" + } + } + }); } - return this.uploadFile(item, filename); + return this.uploadFile(item, opts); + } + + async uploadToEntity( + entity: string, + id: PrimaryFieldType, + field: string, + item: Request | Response | string | File | ReadableStream, + opts?: { + _init?: Omit; + } + ) { + return this.upload(item, { + ...opts, + path: ["entity", entity, id, field] + }); } deleteFile(filename: string) { diff --git a/app/src/media/storage/Storage.ts b/app/src/media/storage/Storage.ts index 1a17c7b..24be8f7 100644 --- a/app/src/media/storage/Storage.ts +++ b/app/src/media/storage/Storage.ts @@ -1,5 +1,6 @@ import { type EmitsEvents, EventManager } from "core/events"; import { type TSchema, isFile } from "core/utils"; +import { isMimeType } from "media/storage/mime-types-tiny"; import * as StorageEvents from "./events"; import type { FileUploadedEventData } from "./events"; @@ -80,37 +81,33 @@ export class Storage implements EmitsEvents { noEmit?: boolean ): Promise { const result = await this.#adapter.putObject(name, file); + if (typeof result === "undefined") { + throw new Error("Failed to upload file"); + } - let info: FileUploadPayload; + let info: FileUploadPayload = { + name, + meta: { + size: 0, + type: "application/octet-stream" + }, + etag: typeof result === "string" ? result : "" + }; - switch (typeof result) { - case "undefined": - throw new Error("Failed to upload file"); - case "string": { - if (isFile(file)) { - info = { - name, - meta: { - size: file.size, - type: file.type - }, - etag: result - }; - break; - } else { - // get object meta - const meta = await this.#adapter.getObjectMeta(name); - if (!meta) { - throw new Error("Failed to get object meta"); - } + if (typeof result === "object") { + info = result; + } else if (isFile(file)) { + info.meta.size = file.size; + info.meta.type = file.type; + } - info = { name, meta, etag: result }; - } - break; + // try to get better meta info + if (!isMimeType(info?.meta.type, ["application/octet-stream", "application/json"])) { + const meta = await this.#adapter.getObjectMeta(name); + if (!meta) { + throw new Error("Failed to get object meta"); } - case "object": - info = result; - break; + info.meta = meta; } const eventData = { diff --git a/app/src/media/utils/index.ts b/app/src/media/utils/index.ts index a560c88..c042acc 100644 --- a/app/src/media/utils/index.ts +++ b/app/src/media/utils/index.ts @@ -17,5 +17,6 @@ export function getRandomizedFilename(file: File | string, length = 16): string throw new Error("Invalid file name"); } + // @todo: use uuid instead? return [randomString(length), getExtension(filename)].filter(Boolean).join("."); } diff --git a/app/src/modules/ModuleApi.ts b/app/src/modules/ModuleApi.ts index 3ba0552..27ad838 100644 --- a/app/src/modules/ModuleApi.ts +++ b/app/src/modules/ModuleApi.ts @@ -169,14 +169,15 @@ export function createResponseProxy( body: Body, data?: Data ): ResponseObject { - const actualData = data ?? (body as unknown as Data); + let actualData: any = data ?? body; const _props = ["raw", "body", "ok", "status", "res", "data", "toJSON"]; + // that's okay, since you have to check res.ok anyway if (typeof actualData !== "object") { - throw new Error(`Response data must be an object, "${typeof actualData}" given.`); + actualData = {}; } - return new Proxy(actualData ?? ({} as any), { + return new Proxy(actualData, { get(target, prop, receiver) { if (prop === "raw" || prop === "res") return raw; if (prop === "body") return body; diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 3f639e5..18cedf9 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -189,7 +189,7 @@ export const Switch = forwardRef< >(({ type, required, ...props }, ref) => { return ( { props.onChange?.({ target: { value: bool } }); }} @@ -203,7 +203,7 @@ export const Switch = forwardRef< } ref={ref} > - + ); });