Compare commits

...

2 Commits

Author SHA1 Message Date
dswbx 7a046fa18e updated references 2025-07-07 08:39:21 +02:00
dswbx 786d1d1bd4 init media adapter refactoring 2025-07-05 18:12:55 +02:00
28 changed files with 189 additions and 178 deletions
+1 -2
View File
@@ -1,5 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { registries } from "../../src";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import * as proto from "../../src/data/prototype"; import * as proto from "../../src/data/prototype";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
@@ -14,8 +13,8 @@ describe("repros", async () => {
* There was an issue that AppData had old configs because of system entity "media" * There was an issue that AppData had old configs because of system entity "media"
*/ */
test("registers media entity correctly to relate to it", async () => { test("registers media entity correctly to relate to it", async () => {
registries.media.register("local", StorageLocalAdapter);
const app = createApp(); const app = createApp();
app.module.media.adapters.set("local", StorageLocalAdapter);
await app.build(); await app.build();
{ {
+2 -5
View File
@@ -1,17 +1,12 @@
/// <reference types="@types/bun" /> /// <reference types="@types/bun" />
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { registries } from "../../src";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import { mergeObject, randomString } from "../../src/core/utils"; import { mergeObject, randomString } from "../../src/core/utils";
import type { TAppMediaConfig } from "../../src/media/media-schema"; 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, disableConsoleLog, enableConsoleLog } from "../helper"; import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
beforeAll(() => {
registries.media.register("local", StorageLocalAdapter);
});
const path = `${assetsPath}/image.png`; const path = `${assetsPath}/image.png`;
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) { async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
@@ -32,6 +27,8 @@ async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
}, },
}); });
app.module.media.adapters.set("local", StorageLocalAdapter);
await app.build(); await app.build();
return app; return app;
} }
+2 -8
View File
@@ -1,22 +1,15 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { registries } from "../../src";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import { em, entity, text } from "../../src/data"; import { em, entity, text } from "../../src/data";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { AppMedia } from "../../src/media/AppMedia"; import { AppMedia } from "../../src/media/AppMedia";
import { mediaConfigSchema } from "../../src/media/media-schema";
import { moduleTestSuite } from "./module-test-suite"; import { moduleTestSuite } from "./module-test-suite";
describe("AppMedia", () => { describe("AppMedia", () => {
test.only("...", () => {
const media = new AppMedia();
console.log(media.toJSON());
});
moduleTestSuite(AppMedia); moduleTestSuite(AppMedia);
test("should allow additional fields", async () => { test("should allow additional fields", async () => {
registries.media.register("local", StorageLocalAdapter); //registries.media.register("local", StorageLocalAdapter);
const app = createApp({ const app = createApp({
initialConfig: { initialConfig: {
@@ -37,6 +30,7 @@ describe("AppMedia", () => {
}).toJSON(), }).toJSON(),
}, },
}); });
app.module.media.adapters.set("local", StorageLocalAdapter);
await app.build(); await app.build();
+7
View File
@@ -30,6 +30,7 @@ export type AppPluginConfig = {
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>; onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
onFirstBoot?: () => MaybePromise<void>; onFirstBoot?: () => MaybePromise<void>;
onBoot?: () => MaybePromise<void>; onBoot?: () => MaybePromise<void>;
onModulesCreated?: (modules: Modules) => void;
}; };
export type AppPlugin = (app: App) => AppPluginConfig; export type AppPlugin = (app: App) => AppPluginConfig;
@@ -113,6 +114,7 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
onFirstBoot: this.onFirstBoot.bind(this), onFirstBoot: this.onFirstBoot.bind(this),
onServerInit: this.onServerInit.bind(this), onServerInit: this.onServerInit.bind(this),
onModulesBuilt: this.onModulesBuilt.bind(this), onModulesBuilt: this.onModulesBuilt.bind(this),
onModulesCreated: this.onModulesCreated.bind(this),
}); });
this.modules.ctx().emgr.registerEvents(AppEvents); this.modules.ctx().emgr.registerEvents(AppEvents);
} }
@@ -325,6 +327,11 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
} }
} }
} }
protected async onModulesCreated(modules: Modules, ctx: ModuleBuildContext) {
await this.runPlugins("onModulesCreated", modules, ctx);
this.options?.manager?.onModulesCreated?.(modules, ctx);
}
} }
export function createApp(config: CreateAppConfig = {}) { export function createApp(config: CreateAppConfig = {}) {
+3 -2
View File
@@ -17,9 +17,8 @@ export async function createApp<Env = BunEnv>(
opts?: RuntimeOptions, opts?: RuntimeOptions,
) { ) {
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static"); const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
registerLocalMediaAdapter();
return await createRuntimeApp( const app = await createRuntimeApp(
{ {
...config, ...config,
serveStatic: serveStatic({ root }), serveStatic: serveStatic({ root }),
@@ -27,6 +26,8 @@ export async function createApp<Env = BunEnv>(
args ?? (process.env as Env), args ?? (process.env as Env),
opts, opts,
); );
registerLocalMediaAdapter(app);
return app;
} }
export function createHandler<Env = BunEnv>( export function createHandler<Env = BunEnv>(
@@ -8,6 +8,7 @@ import { getCached } from "./modes/cached";
import { getDurable } from "./modes/durable"; import { getDurable } from "./modes/durable";
import type { App } from "bknd"; import type { App } from "bknd";
import { $console } from "core/utils"; import { $console } from "core/utils";
import { registerMedia } from "./storage/StorageR2Adapter";
declare global { declare global {
namespace Cloudflare { namespace Cloudflare {
@@ -33,7 +34,7 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
keepAliveSeconds?: number; keepAliveSeconds?: number;
forceHttps?: boolean; forceHttps?: boolean;
manifest?: string; manifest?: string;
registerMedia?: boolean | ((env: Env) => void); registerMedia?: boolean;
}; };
export type Context<Env = CloudflareEnv> = { export type Context<Env = CloudflareEnv> = {
@@ -99,7 +100,16 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
throw new Error(`Unknown mode ${mode}`); throw new Error(`Unknown mode ${mode}`);
} }
registerMediaInternal(app, config, context);
return app.fetch(request, env, ctx); return app.fetch(request, env, ctx);
}, },
}; };
} }
let media_registered: boolean = false;
function registerMediaInternal(app: App, config: CloudflareBkndConfig<any>, ctx?: Context) {
if (!media_registered && config.registerMedia !== false) {
registerMedia(app, ctx?.env as any);
media_registered = true;
}
}
-11
View File
@@ -1,6 +1,5 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import { registerMedia } from "./storage/StorageR2Adapter";
import { getBinding } from "./bindings"; import { getBinding } from "./bindings";
import { d1Sqlite } from "./connection/D1Connection"; import { d1Sqlite } from "./connection/D1Connection";
import { Connection } from "bknd/data"; import { Connection } from "bknd/data";
@@ -88,20 +87,10 @@ export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
}; };
} }
let media_registered: boolean = false;
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>( export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>, config: CloudflareBkndConfig<Env>,
args?: CfMakeConfigArgs<Env>, args?: CfMakeConfigArgs<Env>,
) { ) {
if (!media_registered && config.registerMedia !== false) {
if (typeof config.registerMedia === "function") {
config.registerMedia(args?.env as any);
} else {
registerMedia(args?.env as any);
}
media_registered = true;
}
const appConfig = makeAdapterConfig(config, args?.env); const appConfig = makeAdapterConfig(config, args?.env);
// if connection instance is given, don't do anything // if connection instance is given, don't do anything
-1
View File
@@ -14,7 +14,6 @@ export {
} from "./bindings"; } from "./bindings";
export { constants } from "./config"; export { constants } from "./config";
export { StorageR2Adapter } from "./storage/StorageR2Adapter"; export { StorageR2Adapter } from "./storage/StorageR2Adapter";
export { registries } from "bknd";
// for compatibility with old code // for compatibility with old code
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>( export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
@@ -1,4 +1,4 @@
import { registries } from "bknd"; import type { App } from "bknd";
import { isDebug } from "bknd/core"; import { isDebug } from "bknd/core";
import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media"; import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media";
import { getBindings } from "../bindings"; import { getBindings } from "../bindings";
@@ -13,10 +13,10 @@ export function makeSchema(bindings: string[] = []) {
); );
} }
export function registerMedia(env: Record<string, any>) { export function registerMedia(app: App, env: Record<string, any>) {
const r2_bindings = getBindings(env, "R2Bucket"); const r2_bindings = getBindings(env, "R2Bucket");
registries.media.register( app.module.media.adapters.set(
"r2", "r2",
class extends StorageR2Adapter { class extends StorageR2Adapter {
constructor(private config: any) { constructor(private config: any) {
+3 -2
View File
@@ -4,13 +4,14 @@ import { $console } from "bknd/utils";
import type { MiddlewareHandler } from "hono"; import type { MiddlewareHandler } from "hono";
import type { AdminControllerOptions } from "modules/server/AdminController"; import type { AdminControllerOptions } from "modules/server/AdminController";
import { Connection } from "bknd/data"; import { Connection } from "bknd/data";
import type { MaybePromise } from "core/types";
export { Connection } from "bknd/data"; export { Connection } from "bknd/data";
export type BkndConfig<Args = any> = CreateAppConfig & { export type BkndConfig<Args = any> = CreateAppConfig & {
app?: CreateAppConfig | ((args: Args) => CreateAppConfig); app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
onBuilt?: (app: App) => Promise<void>; onBuilt?: (app: App) => MaybePromise<void>;
beforeBuild?: (app: App) => Promise<void>; beforeBuild?: (app: App) => MaybePromise<void>;
buildConfig?: Parameters<App["build"]>[0]; buildConfig?: Parameters<App["build"]>[0];
}; };
+3 -2
View File
@@ -29,8 +29,7 @@ export async function createApp<Env = NodeEnv>(
console.warn("relativeDistPath is deprecated, please use distPath instead"); console.warn("relativeDistPath is deprecated, please use distPath instead");
} }
registerLocalMediaAdapter(); const app = await createRuntimeApp(
return await createRuntimeApp(
{ {
...config, ...config,
serveStatic: serveStatic({ root }), serveStatic: serveStatic({ root }),
@@ -39,6 +38,8 @@ export async function createApp<Env = NodeEnv>(
args ?? { env: process.env }, args ?? { env: process.env },
opts, opts,
); );
registerLocalMediaAdapter(app);
return app;
} }
export function createHandler<Env = NodeEnv>( export function createHandler<Env = NodeEnv>(
+3 -7
View File
@@ -1,14 +1,10 @@
import { registries } from "bknd"; import type { App } from "bknd";
import { type LocalAdapterConfig, StorageLocalAdapter } from "./StorageLocalAdapter"; import { type LocalAdapterConfig, StorageLocalAdapter } from "./StorageLocalAdapter";
export * from "./StorageLocalAdapter"; export * from "./StorageLocalAdapter";
let registered = false; export function registerLocalMediaAdapter(app: App) {
export function registerLocalMediaAdapter() { app.module.media.adapters.set("local", StorageLocalAdapter);
if (!registered) {
registries.media.register("local", StorageLocalAdapter);
registered = true;
}
return (config: Partial<LocalAdapterConfig> = {}) => { return (config: Partial<LocalAdapterConfig> = {}) => {
const adapter = new StorageLocalAdapter(config); const adapter = new StorageLocalAdapter(config);
+3 -2
View File
@@ -32,8 +32,7 @@ async function createApp<ViteEnv>(
env: ViteEnv = {} as ViteEnv, env: ViteEnv = {} as ViteEnv,
opts: FrameworkOptions = {}, opts: FrameworkOptions = {},
): Promise<App> { ): Promise<App> {
registerLocalMediaAdapter(); const app = await createRuntimeApp(
return await createRuntimeApp(
{ {
...config, ...config,
adminOptions: config.adminOptions ?? { adminOptions: config.adminOptions ?? {
@@ -49,6 +48,8 @@ async function createApp<ViteEnv>(
env, env,
opts, opts,
); );
registerLocalMediaAdapter(app);
return app;
} }
export function serve<ViteEnv>( export function serve<ViteEnv>(
+4 -9
View File
@@ -1,11 +1,10 @@
import type { Config } from "@libsql/client/node"; import type { Config } from "@libsql/client/node";
import type { App, CreateAppConfig } from "App"; import type { App, CreateAppConfig } from "App";
import { StorageLocalAdapter } from "adapter/node/storage"; import { registerLocalMediaAdapter } from "adapter/node/storage";
import type { CliBkndConfig, CliCommand } from "cli/types"; import type { CliBkndConfig, CliCommand } from "cli/types";
import { Option } from "commander"; import { Option } from "commander";
import { config } from "core"; import { config } from "core";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { registries } from "modules/registries";
import c from "picocolors"; import c from "picocolors";
import path from "node:path"; import path from "node:path";
import { import {
@@ -57,12 +56,6 @@ export const run: CliCommand = (program) => {
.action(action); .action(action);
}; };
// automatically register local adapter
const local = StorageLocalAdapter.prototype.getName();
if (!registries.media.has(local)) {
registries.media.register(local, StorageLocalAdapter);
}
type MakeAppConfig = { type MakeAppConfig = {
connection?: CreateAppConfig["connection"]; connection?: CreateAppConfig["connection"];
server?: { platform?: Platform }; server?: { platform?: Platform };
@@ -71,10 +64,12 @@ type MakeAppConfig = {
}; };
async function makeApp(config: MakeAppConfig) { async function makeApp(config: MakeAppConfig) {
return await createRuntimeApp({ const app = await createRuntimeApp({
serveStatic: await serveStatic(config.server?.platform ?? "node"), serveStatic: await serveStatic(config.server?.platform ?? "node"),
...config, ...config,
}); });
registerLocalMediaAdapter(app);
return app;
} }
export async function makeConfigApp(_config: CliBkndConfig, platform?: Platform) { export async function makeConfigApp(_config: CliBkndConfig, platform?: Platform) {
+1 -1
View File
@@ -29,7 +29,7 @@ export class SchemaObject<Schema extends TSchema = TSchema> {
this._default = _schema.template({}, { withOptional: true }) as any; this._default = _schema.template({}, { withOptional: true }) as any;
this._value = parse(_schema, structuredClone(initial ?? {}), { this._value = parse(_schema, structuredClone(initial ?? {}), {
withDefaults: true, withDefaults: true,
withExtendedDefaults: true, //withExtendedDefaults: true,
forceParse: this.isForceParse(), forceParse: this.isForceParse(),
skipMark: this.isForceParse(), skipMark: this.isForceParse(),
}); });
-1
View File
@@ -34,7 +34,6 @@ export type { ServerEnv } from "modules/Controller";
export type { BkndConfig } from "bknd/adapter"; export type { BkndConfig } from "bknd/adapter";
export * as middlewares from "modules/middlewares"; export * as middlewares from "modules/middlewares";
export { registries } from "modules/registries";
export type { MediaFieldSchema } from "media/AppMedia"; export type { MediaFieldSchema } from "media/AppMedia";
export type { UserFieldSchema } from "auth/AppAuth"; export type { UserFieldSchema } from "auth/AppAuth";
+48 -6
View File
@@ -1,12 +1,15 @@
import type { AppEntity } from "core"; import type { AppEntity, Constructor } from "core";
import { $console } from "core/utils"; import { $console, objectTransform } from "core/utils";
import type { Entity, EntityManager } from "data"; import type { Entity, EntityManager } from "data";
import { type FileUploadedEventData, Storage, type StorageAdapter, MediaPermissions } from "media"; import { type FileUploadedEventData, Storage, type StorageAdapter, MediaPermissions } from "media";
import { Module } from "modules/Module"; import { Module, type ModuleBuildContext } from "modules/Module";
import { type FieldSchema, em, entity } from "../data/prototype"; import { type FieldSchema, em, entity } from "../data/prototype";
import { MediaController } from "./api/MediaController"; import { MediaController } from "./api/MediaController";
import { buildMediaSchema, type mediaConfigSchema, registry, type TAppMediaConfig } from "./media-schema"; import { mediaConfigSchema, type TAppMediaConfig } from "./media-schema";
import { mediaFields } from "./media-entities"; import { mediaFields } from "./media-entities";
import { StorageS3Adapter } from "media/storage/adapters/s3/StorageS3Adapter";
import { StorageCloudinaryAdapter } from "media/storage/adapters/cloudinary/StorageCloudinaryAdapter";
import { s } from "core/object/schema";
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>; export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
declare module "core" { declare module "core" {
@@ -15,10 +18,18 @@ declare module "core" {
media: Media; media: Media;
} }
} }
type ClassThatImplements<T> = Constructor<T> & { prototype: T };
// @todo: current workaround to make it all required // @todo: current workaround to make it all required
export class AppMedia extends Module<Required<TAppMediaConfig>> { export class AppMedia extends Module<Required<TAppMediaConfig>> {
private _storage?: Storage; private _storage?: Storage;
adapters: Map<string, ClassThatImplements<StorageAdapter>> = new Map();
constructor(initial?: Partial<TAppMediaConfig>, _ctx?: ModuleBuildContext) {
super(initial, _ctx);
this.adapters.set("s3", StorageS3Adapter);
this.adapters.set("cloudinary", StorageCloudinaryAdapter);
}
override async build() { override async build() {
if (!this.config.enabled) { if (!this.config.enabled) {
@@ -35,7 +46,11 @@ export class AppMedia extends Module<Required<TAppMediaConfig>> {
let adapter: StorageAdapter; let adapter: StorageAdapter;
try { try {
const { type, config } = this.config.adapter; const { type, config } = this.config.adapter;
const cls = registry.get(type as any).cls; const cls = this.adapters.get(type as any);
if (!cls) {
throw new Error(`Adapter ${type} not found`);
}
adapter = new cls(config as any); adapter = new cls(config as any);
this._storage = new Storage(adapter, this.config.storage, this.ctx.emgr); this._storage = new Storage(adapter, this.config.storage, this.ctx.emgr);
@@ -59,7 +74,34 @@ export class AppMedia extends Module<Required<TAppMediaConfig>> {
} }
getSchema() { getSchema() {
return buildMediaSchema(); if (!this.adapters) {
return mediaConfigSchema;
}
const adapterSchemaObject = objectTransform(
Object.fromEntries(this.adapters.entries()),
(adapter, name) => {
const schema = adapter.prototype.getSchema();
if (!schema) {
throw new Error(`Adapter ${name} has no schema`);
}
return s.strictObject(
{
type: s.literal(name),
config: schema,
},
{
title: String(schema.title ?? name),
description: schema.description,
},
);
},
);
return s.strictObject({
...mediaConfigSchema.properties,
adapter: s.anyOf(Object.values(adapterSchemaObject)).optional(),
});
} }
get basepath() { get basepath() {
-26
View File
@@ -1,5 +1,3 @@
import { type Constructor, Registry } from "core";
export { guess as guessMimeType } from "./storage/mime-types-tiny"; export { guess as guessMimeType } from "./storage/mime-types-tiny";
export { export {
Storage, Storage,
@@ -15,7 +13,6 @@ import {
StorageCloudinaryAdapter, StorageCloudinaryAdapter,
} from "./storage/adapters/cloudinary/StorageCloudinaryAdapter"; } from "./storage/adapters/cloudinary/StorageCloudinaryAdapter";
import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/s3/StorageS3Adapter"; import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/s3/StorageS3Adapter";
import type { s } from "core/object/schema";
export { StorageAdapter }; export { StorageAdapter };
export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig }; export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig };
@@ -25,27 +22,4 @@ export * as MediaPermissions from "./media-permissions";
export type { FileUploadedEventData } from "./storage/events"; export type { FileUploadedEventData } from "./storage/events";
export * from "./utils"; export * from "./utils";
type ClassThatImplements<T> = Constructor<T> & { prototype: T };
export const MediaAdapterRegistry = new Registry<{
cls: ClassThatImplements<StorageAdapter>;
schema: s.Schema;
}>((cls: ClassThatImplements<StorageAdapter>) => ({
cls,
schema: cls.prototype.getSchema() as s.Schema,
}))
.register("s3", StorageS3Adapter)
.register("cloudinary", StorageCloudinaryAdapter);
export const Adapters = {
s3: {
cls: StorageS3Adapter,
schema: StorageS3Adapter.prototype.getSchema(),
},
cloudinary: {
cls: StorageCloudinaryAdapter,
schema: StorageCloudinaryAdapter.prototype.getSchema(),
},
} as const;
export { adapterTestSuite } from "./storage/adapters/adapter-test-suite"; export { adapterTestSuite } from "./storage/adapters/adapter-test-suite";
+24 -45
View File
@@ -1,50 +1,29 @@
import { objectTransform } from "core/utils";
import { Adapters } from "media";
import { registries } from "modules/registries";
import { s } from "core/object/schema"; import { s } from "core/object/schema";
export const ADAPTERS = { export const mediaConfigSchema = s.strictObject(
...Adapters, {
} as const; enabled: s.boolean({ default: false }),
basepath: s.string({ default: "/api/media" }),
export const registry = registries.media; entity_name: s.string({ default: "media" }),
storage: s.strictObject(
export function buildMediaSchema() {
const adapterSchemaObject = objectTransform(registry.all(), (adapter, name) => {
return s.strictObject(
{ {
type: s.literal(name), body_max_size: s
config: adapter.schema, .number({
description: "Max size of the body in bytes. Leave blank for unlimited.",
})
.optional(),
}, },
{ { default: {} },
title: adapter.schema?.title ?? name, ),
description: adapter.schema?.description, adapter: s
}, .strictObject({
); type: s.string(),
}); config: s.any(),
})
return s.strictObject( .optional(),
{ },
enabled: s.boolean({ default: false }), {
basepath: s.string({ default: "/api/media" }), default: {},
entity_name: s.string({ default: "media" }), },
storage: s.strictObject( );
{
body_max_size: s
.number({
description: "Max size of the body in bytes. Leave blank for unlimited.",
})
.optional(),
},
{ default: {} },
),
adapter: s.anyOf(Object.values(adapterSchemaObject)).optional(),
},
{
default: {},
},
);
}
export const mediaConfigSchema = buildMediaSchema();
export type TAppMediaConfig = s.Static<typeof mediaConfigSchema>; export type TAppMediaConfig = s.Static<typeof mediaConfigSchema>;
+3
View File
@@ -38,8 +38,11 @@ export abstract class Module<Schema extends object = object> {
overwritePaths: this.getOverwritePaths(), overwritePaths: this.getOverwritePaths(),
onBeforeUpdate: this.onBeforeUpdate.bind(this), onBeforeUpdate: this.onBeforeUpdate.bind(this),
}); });
this.init();
} }
init() {}
static ctx_flags = { static ctx_flags = {
sync_required: false, sync_required: false,
ctx_reload_required: false, ctx_reload_required: false,
+21 -5
View File
@@ -20,7 +20,7 @@ import { AppMedia } from "../media/AppMedia";
import type { ServerEnv } from "./Controller"; import type { ServerEnv } from "./Controller";
import { Module, type ModuleBuildContext } from "./Module"; import { Module, type ModuleBuildContext } from "./Module";
import { ModuleHelper } from "./ModuleHelper"; import { ModuleHelper } from "./ModuleHelper";
import { s, mark, stripMark } from "core/object/schema"; import { s } from "core/object/schema";
export type { ModuleBuildContext }; export type { ModuleBuildContext };
@@ -80,6 +80,8 @@ export type ModuleManagerOptions = {
seed?: (ctx: ModuleBuildContext) => Promise<void>; seed?: (ctx: ModuleBuildContext) => Promise<void>;
// called right after modules are built, before finish // called right after modules are built, before finish
onModulesBuilt?: (ctx: ModuleBuildContext) => Promise<void>; onModulesBuilt?: (ctx: ModuleBuildContext) => Promise<void>;
// called right after modules are created, before build
onModulesCreated?: (modules: Modules, ctx: ModuleBuildContext) => void;
/** @deprecated */ /** @deprecated */
verbosity?: Verbosity; verbosity?: Verbosity;
}; };
@@ -127,8 +129,16 @@ export class ModuleManagerConfigUpdateEvent<
}> { }> {
static override slug = "mm-config-update"; static override slug = "mm-config-update";
} }
export class ModuleManagerModulesCreatedEvent extends ModuleManagerEvent<{
modules: Modules;
ctx: ModuleBuildContext;
}> {
static override slug = "mm-modules-created";
}
export const ModuleManagerEvents = { export const ModuleManagerEvents = {
ModuleManagerConfigUpdateEvent, ModuleManagerConfigUpdateEvent,
ModuleManagerModulesCreatedEvent,
}; };
// @todo: cleanup old diffs on upgrade // @todo: cleanup old diffs on upgrade
@@ -166,7 +176,8 @@ export class ModuleManager {
if ("version" in options.initial) { if ("version" in options.initial) {
const { version, ...initialConfig } = options.initial; const { version, ...initialConfig } = options.initial;
this._version = version; this._version = version;
initial = stripMark(initialConfig); //initial = stripMark(initialConfig);
initial = initialConfig;
this._booted_with = "provided"; this._booted_with = "provided";
} else { } else {
@@ -194,6 +205,11 @@ export class ModuleManager {
this.modules[key] = module; this.modules[key] = module;
} }
if (this.options?.onModulesCreated) {
this.options.onModulesCreated(this.modules, context);
}
this.logger.log("modules created"); this.logger.log("modules created");
} catch (e) { } catch (e) {
this.logger.log("failed to create modules", e); this.logger.log("failed to create modules", e);
@@ -487,10 +503,11 @@ export class ModuleManager {
// set version and config from fetched // set version and config from fetched
this._version = result.version; this._version = result.version;
if (this.options?.trustFetched === true) { // @todo: add back?
/* if (this.options?.trustFetched === true) {
this.logger.log("trusting fetched config (mark)"); this.logger.log("trusting fetched config (mark)");
mark(result.json); mark(result.json);
} } */
// if version doesn't match, migrate before building // if version doesn't match, migrate before building
if (this.version() !== CURRENT_VERSION) { if (this.version() !== CURRENT_VERSION) {
@@ -732,7 +749,6 @@ export function getDefaultSchema() {
export function getDefaultConfig(): ModuleConfigs { export function getDefaultConfig(): ModuleConfigs {
const config = transformObject(MODULES, (module) => { const config = transformObject(MODULES, (module) => {
return module.prototype.getSchema().template(); return module.prototype.getSchema().template();
//return Default(module.prototype.getSchema(), {});
}); });
return config as any; return config as any;
-7
View File
@@ -1,7 +0,0 @@
import { MediaAdapterRegistry } from "media";
const registries = {
media: MediaAdapterRegistry,
} as const;
export { registries };
@@ -64,12 +64,13 @@ const FieldImpl = ({
const id = `${name}-${useId()}`; const id = `${name}-${useId()}`;
const required = typeof _required === "boolean" ? _required : ctx.required; const required = typeof _required === "boolean" ? _required : ctx.required;
if (!isTypeSchema(schema)) if (!schema) {
return ( return (
<Pre> <Pre>
[Field] {path} has no schema ({JSON.stringify(schema)}) [Field] {path} has no schema ({JSON.stringify(schema)})
</Pre> </Pre>
); );
}
if (isType(schema.type, "object")) { if (isType(schema.type, "object")) {
return <ObjectField path={name} />; return <ObjectField path={name} />;
+1 -1
View File
@@ -49,5 +49,5 @@
"__test__", "__test__",
"e2e/**/*.ts" "e2e/**/*.ts"
], ],
"exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"] "exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts", "**/dist/**"]
} }
+10 -5
View File
@@ -1,8 +1,8 @@
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import { serveStatic } from "@hono/node-server/serve-static"; import { serveStatic } from "@hono/node-server/serve-static";
import { showRoutes } from "hono/dev"; import { showRoutes } from "hono/dev";
import { App, registries } from "./src"; import { App } from "./src/App";
import { StorageLocalAdapter } from "./src/adapter/node"; import { StorageLocalAdapter } from "./src/adapter/node/storage/StorageLocalAdapter";
import type { Connection } from "./src/data/connection/Connection"; import type { Connection } from "./src/data/connection/Connection";
import { __bknd } from "modules/ModuleManager"; import { __bknd } from "modules/ModuleManager";
import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection"; import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection";
@@ -10,8 +10,6 @@ import { libsql } from "./src/data/connection/sqlite/libsql/LibsqlConnection";
import { $console } from "core/utils"; import { $console } from "core/utils";
import { createClient } from "@libsql/client"; import { createClient } from "@libsql/client";
registries.media.register("local", StorageLocalAdapter);
const example = import.meta.env.VITE_EXAMPLE; const example = import.meta.env.VITE_EXAMPLE;
let connection: Connection; let connection: Connection;
@@ -70,7 +68,7 @@ if (import.meta.env.VITE_DB_LIBSQL_URL) {
} */ } */
let app: App; let app: App;
const recreate = import.meta.env.VITE_APP_FRESH === "1"; const recreate = true; //import.meta.env.VITE_APP_FRESH === "1";
const debugRerenders = import.meta.env.VITE_DEBUG_RERENDERS === "1"; const debugRerenders = import.meta.env.VITE_DEBUG_RERENDERS === "1";
let firstStart = true; let firstStart = true;
export default { export default {
@@ -78,6 +76,13 @@ export default {
if (!app || recreate) { if (!app || recreate) {
app = App.create({ app = App.create({
connection, connection,
options: {
manager: {
onModulesCreated: (modules, ctx) => {
modules.media.adapters.set("local", StorageLocalAdapter);
},
},
},
}); });
app.emgr.onEvent( app.emgr.onEvent(
App.Events.AppBuiltEvent, App.Events.AppBuiltEvent,
+9 -6
View File
@@ -3,9 +3,6 @@ import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { boolean, em, entity, text } from "bknd/data"; import { boolean, em, entity, text } from "bknd/data";
import { secureRandomString } from "bknd/utils"; import { secureRandomString } from "bknd/utils";
// since we're running in node, we can register the local media adapter
const local = registerLocalMediaAdapter();
// the em() function makes it easy to create an initial schema // the em() function makes it easy to create an initial schema
const schema = em({ const schema = em({
todos: entity("todos", { todos: entity("todos", {
@@ -41,11 +38,17 @@ export default {
// ... and media // ... and media
media: { media: {
enabled: true, enabled: true,
adapter: local({ adapter: {
path: "./public/uploads", type: "local",
}), config: {
path: "./public/uploads",
},
},
}, },
}, },
beforeBuild: (app) => {
registerLocalMediaAdapter(app);
},
options: { options: {
// the seed option is only executed if the database was empty // the seed option is only executed if the database was empty
seed: async (ctx) => { seed: async (ctx) => {
+15 -13
View File
@@ -3,16 +3,6 @@ import { boolean, em, entity, text } from "bknd/data";
import { registerLocalMediaAdapter } from "bknd/adapter/node"; import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { secureRandomString } from "bknd/utils"; import { secureRandomString } from "bknd/utils";
// The local media adapter works well in development, and server based
// deployments. However, on vercel or any other serverless deployments,
// you shouldn't use a filesystem based media adapter.
//
// Additionally, if you run the bknd api on the "edge" runtime,
// this would not work as well.
//
// For production, it is recommended to uncomment the line below.
const local = registerLocalMediaAdapter();
const schema = em({ const schema = em({
todos: entity("todos", { todos: entity("todos", {
title: text(), title: text(),
@@ -50,11 +40,23 @@ export default {
// ... and media // ... and media
media: { media: {
enabled: true, enabled: true,
adapter: local({ adapter: {
path: "./public/uploads", type: "local",
}), config: {
path: "./public/uploads",
},
},
}, },
}, },
beforeBuild: (app) => {
// The local media adapter works well in development, and server based
// deployments. However, on vercel or any other serverless deployments,
// you shouldn't use a filesystem based media adapter.
//
// Additionally, if you run the bknd api on the "edge" runtime,
// this would not work as well.
registerLocalMediaAdapter(app);
},
options: { options: {
// the seed option is only executed if the database was empty // the seed option is only executed if the database was empty
seed: async (ctx) => { seed: async (ctx) => {
+10 -6
View File
@@ -3,9 +3,6 @@ import type { ReactRouterBkndConfig } from "bknd/adapter/react-router";
import { boolean, em, entity, text } from "bknd/data"; import { boolean, em, entity, text } from "bknd/data";
import { secureRandomString } from "bknd/utils"; import { secureRandomString } from "bknd/utils";
// since we're running in node, we can register the local media adapter
const local = registerLocalMediaAdapter();
const schema = em({ const schema = em({
todos: entity("todos", { todos: entity("todos", {
title: text(), title: text(),
@@ -40,11 +37,18 @@ export default {
// ... and media // ... and media
media: { media: {
enabled: true, enabled: true,
adapter: local({ adapter: {
path: "./public/uploads", type: "local",
}), config: {
path: "./public/uploads",
},
},
}, },
}, },
beforeBuild: (app) => {
// since we're running in node, we can register the local media adapter
registerLocalMediaAdapter(app);
},
options: { options: {
// the seed option is only executed if the database was empty // the seed option is only executed if the database was empty
seed: async (ctx) => { seed: async (ctx) => {