diff --git a/app/__test__/core/Registry.spec.ts b/app/__test__/core/Registry.spec.ts index 1be9310..557b39a 100644 --- a/app/__test__/core/Registry.spec.ts +++ b/app/__test__/core/Registry.spec.ts @@ -1,4 +1,4 @@ -import { describe, test } from "bun:test"; +import { describe, expect, test } from "bun:test"; import type { TObject, TString } from "@sinclair/typebox"; import { Registry } from "../../src/core/registry/Registry"; import { type TSchema, Type } from "../../src/core/utils"; @@ -11,6 +11,9 @@ class What { method() { return null; } + getType() { + return Type.Object({ type: Type.String() }); + } } class What2 extends What {} class NotAllowed {} @@ -32,25 +35,53 @@ describe("Registry", () => { } satisfies Record); const item = registry.get("first"); + expect(item).toBeDefined(); + expect(item?.cls).toBe(What); + const second = Type.Object({ type: Type.String(), what: Type.String() }); registry.add("second", { cls: What2, - schema: Type.Object({ type: Type.String(), what: Type.String() }), + schema: second, enabled: true }); + // @ts-ignore + expect(registry.get("second").schema).toEqual(second); + + const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() }); registry.add("third", { // @ts-expect-error cls: NotAllowed, - schema: Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() }), + schema: third, enabled: true }); + // @ts-ignore + expect(registry.get("third").schema).toEqual(third); + + const fourth = Type.Object({ type: Type.Number(), what22: Type.String() }); registry.add("fourth", { cls: What, // @ts-expect-error - schema: Type.Object({ type: Type.Number(), what22: Type.String() }), + schema: fourth, enabled: true }); + // @ts-ignore + expect(registry.get("fourth").schema).toEqual(fourth); - console.log("list", registry.all()); + expect(Object.keys(registry.all()).length).toBe(4); + }); + + test("uses registration fn", async () => { + const registry = new Registry((a: ClassRef) => { + return { + cls: a, + schema: a.prototype.getType(), + enabled: true + }; + }); + + registry.register("what2", What2); + expect(registry.get("what2")).toBeDefined(); + expect(registry.get("what2").cls).toBe(What2); + expect(registry.get("what2").schema).toEqual(What2.prototype.getType()); }); }); diff --git a/app/src/adapter/bun/bun.adapter.ts b/app/src/adapter/bun/bun.adapter.ts index ae14be5..05079e2 100644 --- a/app/src/adapter/bun/bun.adapter.ts +++ b/app/src/adapter/bun/bun.adapter.ts @@ -6,14 +6,25 @@ import type { Serve, ServeOptions } from "bun"; import { serveStatic } from "hono/bun"; let app: App; -export async function createApp(_config: Partial = {}, distPath?: string) { +export type ExtendedAppCreateConfig = Partial & { + distPath?: string; + onBuilt?: (app: App) => Promise; + buildOptions?: Parameters[0]; +}; + +export async function createApp({ + distPath, + onBuilt, + buildOptions, + ...config +}: ExtendedAppCreateConfig) { const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static"); if (!app) { - app = App.create(_config); + app = App.create(config); - app.emgr.on( - "app-built", + app.emgr.onEvent( + App.Events.AppBuiltEvent, async () => { app.modules.server.get( "/*", @@ -22,20 +33,18 @@ export async function createApp(_config: Partial = {}, distPath }) ); app.registerAdminController(); + await onBuilt?.(app); }, "sync" ); - await app.build(); + await app.build(buildOptions); } return app; } -export type BunAdapterOptions = Omit & - CreateAppConfig & { - distPath?: string; - }; +export type BunAdapterOptions = Omit & ExtendedAppCreateConfig; export function serve({ distPath, @@ -44,13 +53,23 @@ export function serve({ plugins, options, port = 1337, + onBuilt, + buildOptions, ...serveOptions }: BunAdapterOptions = {}) { Bun.serve({ ...serveOptions, port, fetch: async (request: Request) => { - const app = await createApp({ connection, initialConfig, plugins, options }, distPath); + const app = await createApp({ + connection, + initialConfig, + plugins, + options, + onBuilt, + buildOptions, + distPath + }); return app.fetch(request); } }); diff --git a/app/src/adapter/node/index.ts b/app/src/adapter/node/index.ts index 47d4c97..be24456 100644 --- a/app/src/adapter/node/index.ts +++ b/app/src/adapter/node/index.ts @@ -1,59 +1,5 @@ -import path from "node:path"; -import { serve as honoServe } from "@hono/node-server"; -import { serveStatic } from "@hono/node-server/serve-static"; -import { App, type CreateAppConfig } from "bknd"; - -export type NodeAdapterOptions = CreateAppConfig & { - relativeDistPath?: string; - port?: number; - hostname?: string; - listener?: Parameters[1]; -}; - -export function serve({ - relativeDistPath, - port = 1337, - hostname, - listener, - ...config -}: NodeAdapterOptions = {}) { - const root = path.relative( - process.cwd(), - path.resolve(relativeDistPath ?? "./node_modules/bknd/dist", "static") - ); - let app: App; - - honoServe( - { - port, - hostname, - fetch: async (req: Request) => { - if (!app) { - app = App.create(config); - - app.emgr.on( - "app-built", - async () => { - app.modules.server.get( - "/*", - serveStatic({ - root - }) - ); - app.registerAdminController(); - }, - "sync" - ); - - await app.build(); - } - - return app.fetch(req); - } - }, - (connInfo) => { - console.log(`Server is running on http://localhost:${connInfo.port}`); - listener?.(connInfo); - } - ); -} +export * from "./node.adapter"; +export { + StorageLocalAdapter, + type LocalAdapterConfig +} from "../../media/storage/adapters/StorageLocalAdapter"; diff --git a/app/src/adapter/node/node.adapter.ts b/app/src/adapter/node/node.adapter.ts new file mode 100644 index 0000000..6cd6ab1 --- /dev/null +++ b/app/src/adapter/node/node.adapter.ts @@ -0,0 +1,64 @@ +import path from "node:path"; +import { serve as honoServe } from "@hono/node-server"; +import { serveStatic } from "@hono/node-server/serve-static"; +import { App, type CreateAppConfig } from "bknd"; + +export type NodeAdapterOptions = CreateAppConfig & { + relativeDistPath?: string; + port?: number; + hostname?: string; + listener?: Parameters[1]; + onBuilt?: (app: App) => Promise; + buildOptions?: Parameters[0]; +}; + +export function serve({ + relativeDistPath, + port = 1337, + hostname, + listener, + onBuilt, + buildOptions = {}, + ...config +}: NodeAdapterOptions = {}) { + const root = path.relative( + process.cwd(), + path.resolve(relativeDistPath ?? "./node_modules/bknd/dist", "static") + ); + let app: App; + + honoServe( + { + port, + hostname, + fetch: async (req: Request) => { + if (!app) { + app = App.create(config); + + app.emgr.onEvent( + App.Events.AppBuiltEvent, + async () => { + app.modules.server.get( + "/*", + serveStatic({ + root + }) + ); + app.registerAdminController(); + await onBuilt?.(app); + }, + "sync" + ); + + await app.build(buildOptions); + } + + return app.fetch(req); + } + }, + (connInfo) => { + console.log(`Server is running on http://localhost:${connInfo.port}`); + listener?.(connInfo); + } + ); +} diff --git a/app/src/adapter/vite/vite.adapter.ts b/app/src/adapter/vite/vite.adapter.ts index 6faaefe..448f50d 100644 --- a/app/src/adapter/vite/vite.adapter.ts +++ b/app/src/adapter/vite/vite.adapter.ts @@ -8,8 +8,8 @@ function createApp(config: BkndConfig, env: any) { } function setAppBuildListener(app: App, config: BkndConfig, html?: string) { - app.emgr.on( - "app-built", + app.emgr.onEvent( + App.Events.AppBuiltEvent, async () => { await config.onBuilt?.(app); if (config.setAdminHtml) { diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 426023b..46fa586 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -220,15 +220,23 @@ export class Authenticator = Record< } private async getAuthCookie(c: Context): Promise { - const secret = this.config.jwt.secret; + try { + const secret = this.config.jwt.secret; + + const token = await getSignedCookie(c, secret, "auth"); + if (typeof token !== "string") { + await deleteCookie(c, "auth", this.cookieOptions); + return undefined; + } + + return token; + } catch (e: any) { + if (e instanceof Error) { + console.error("[Error:getAuthCookie]", e.message); + } - const token = await getSignedCookie(c, secret, "auth"); - if (typeof token !== "string") { - await deleteCookie(c, "auth", this.cookieOptions); return undefined; } - - return token; } async requestCookieRefresh(c: Context) { diff --git a/app/src/cli/commands/run/run.ts b/app/src/cli/commands/run/run.ts index 7ad9568..2e1fb32 100644 --- a/app/src/cli/commands/run/run.ts +++ b/app/src/cli/commands/run/run.ts @@ -1,8 +1,10 @@ import type { Config } from "@libsql/client/node"; import { App, type CreateAppConfig } from "App"; import type { BkndConfig } from "adapter"; +import { StorageLocalAdapter } from "adapter/node"; import type { CliCommand } from "cli/types"; import { Option } from "commander"; +import { registries } from "modules/registries"; import { PLATFORMS, type Platform, @@ -37,6 +39,12 @@ export const run: CliCommand = (program) => { .action(action); }; +// automatically register local adapter +const local = StorageLocalAdapter.prototype.getName(); +if (!registries.media.has(local)) { + registries.media.register(local, StorageLocalAdapter); +} + type MakeAppConfig = { connection?: CreateAppConfig["connection"]; server?: { platform?: Platform }; @@ -47,8 +55,8 @@ type MakeAppConfig = { async function makeApp(config: MakeAppConfig) { const app = App.create({ connection: config.connection }); - app.emgr.on( - "app-built", + app.emgr.onEvent( + App.Events.AppBuiltEvent, async () => { await attachServeStatic(app, config.server?.platform ?? "node"); app.registerAdminController(); @@ -68,8 +76,8 @@ export async function makeConfigApp(config: BkndConfig, platform?: Platform) { const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app; const app = App.create(appConfig); - app.emgr.on( - "app-built", + app.emgr.onEvent( + App.Events.AppBuiltEvent, async () => { await attachServeStatic(app, platform ?? "node"); app.registerAdminController(); diff --git a/app/src/core/object/SchemaObject.ts b/app/src/core/object/SchemaObject.ts index c70ef28..aad5b14 100644 --- a/app/src/core/object/SchemaObject.ts +++ b/app/src/core/object/SchemaObject.ts @@ -69,7 +69,8 @@ export class SchemaObject { forceParse: true, skipMark: this.isForceParse() }); - const updatedConfig = noEmit ? valid : await this.onBeforeUpdate(this._config, valid); + // regardless of "noEmit" – this should always be triggered + const updatedConfig = await this.onBeforeUpdate(this._config, valid); this._value = updatedConfig; this._config = Object.freeze(updatedConfig); diff --git a/app/src/core/registry/Registry.ts b/app/src/core/registry/Registry.ts index 12209ca..b895f51 100644 --- a/app/src/core/registry/Registry.ts +++ b/app/src/core/registry/Registry.ts @@ -1,29 +1,50 @@ export type Constructor = new (...args: any[]) => T; -export class Registry = Record> { + +export type RegisterFn = (unknown: any) => Item; + +export class Registry< + Item, + Items extends Record = Record, + Fn extends RegisterFn = RegisterFn +> { private is_set: boolean = false; private items: Items = {} as Items; - set>(items: Actual) { + constructor(private registerFn?: Fn) {} + + set>(items: Actual) { if (this.is_set) { throw new Error("Registry is already set"); } - // @ts-ignore - this.items = items; + this.items = items as unknown as Items; this.is_set = true; - return this as unknown as Registry; + return this as unknown as Registry; } add(name: string, item: Item) { - // @ts-ignore - this.items[name] = item; + this.items[name as keyof Items] = item as Items[keyof Items]; return this; } + register(name: string, specific: Parameters[0]) { + if (this.registerFn) { + const item = this.registerFn(specific); + this.items[name as keyof Items] = item as Items[keyof Items]; + return this; + } + + return this.add(name, specific); + } + get(name: Name): Items[Name] { return this.items[name]; } + has(name: keyof Items): boolean { + return name in this.items; + } + all() { return this.items; } diff --git a/app/src/index.ts b/app/src/index.ts index 578ab5d..78e9f70 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -7,5 +7,7 @@ export { type ModuleSchemas } from "modules/ModuleManager"; +export { registries } from "modules/registries"; + export type * from "./adapter"; export { Api, type ApiOptions } from "./Api"; diff --git a/app/src/media/index.ts b/app/src/media/index.ts index bdc94c1..7a1cdc7 100644 --- a/app/src/media/index.ts +++ b/app/src/media/index.ts @@ -17,10 +17,6 @@ import { import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/StorageS3Adapter"; export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig }; -/*export { - StorageLocalAdapter, - type LocalAdapterConfig -} from "./storage/adapters/StorageLocalAdapter";*/ export * as StorageEvents from "./storage/events"; export { type FileUploadedEventData } from "./storage/events"; @@ -31,16 +27,12 @@ type ClassThatImplements = Constructor & { prototype: T }; export const MediaAdapterRegistry = new Registry<{ cls: ClassThatImplements; schema: TObject; -}>().set({ - s3: { - cls: StorageS3Adapter, - schema: StorageS3Adapter.prototype.getSchema() - }, - cloudinary: { - cls: StorageCloudinaryAdapter, - schema: StorageCloudinaryAdapter.prototype.getSchema() - } -}); +}>((cls: ClassThatImplements) => ({ + cls, + schema: cls.prototype.getSchema() as TObject +})) + .register("s3", StorageS3Adapter) + .register("cloudinary", StorageCloudinaryAdapter); export const Adapters = { s3: { diff --git a/app/src/media/storage/adapters/StorageLocalAdapter/StorageLocalAdapter.ts b/app/src/media/storage/adapters/StorageLocalAdapter/StorageLocalAdapter.ts index f6c1bb1..f483eeb 100644 --- a/app/src/media/storage/adapters/StorageLocalAdapter/StorageLocalAdapter.ts +++ b/app/src/media/storage/adapters/StorageLocalAdapter/StorageLocalAdapter.ts @@ -1,17 +1,11 @@ import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises"; import { type Static, Type, parse } from "core/utils"; -import type { - FileBody, - FileListObject, - FileMeta, - FileUploadPayload, - StorageAdapter -} from "../../Storage"; +import type { FileBody, FileListObject, FileMeta, StorageAdapter } from "../../Storage"; import { guessMimeType } from "../../mime-types"; export const localAdapterConfig = Type.Object( { - path: Type.String() + path: Type.String({ default: "./" }) }, { title: "Local" } ); diff --git a/app/vite.dev.ts b/app/vite.dev.ts index 608e24e..6050997 100644 --- a/app/vite.dev.ts +++ b/app/vite.dev.ts @@ -1,14 +1,10 @@ import { serveStatic } from "@hono/node-server/serve-static"; import { createClient } from "@libsql/client/node"; -import { App } from "./src"; +import { App, registries } from "./src"; import { LibsqlConnection } from "./src/data"; import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter"; -import { registries } from "./src/modules/registries"; -registries.media.add("local", { - cls: StorageLocalAdapter, - schema: StorageLocalAdapter.prototype.getSchema() -}); +registries.media.register("local", StorageLocalAdapter); const credentials = { url: import.meta.env.VITE_DB_URL!, @@ -24,8 +20,8 @@ export default { async fetch(request: Request) { const app = App.create({ connection }); - app.emgr.on( - "app-built", + app.emgr.onEvent( + App.Events.AppBuiltEvent, async () => { app.registerAdminController({ forceDev: true }); app.module.server.client.get("/assets/*", serveStatic({ root: "./" }));