updated examples: astro, nextjs, remix, bun, node

This commit is contained in:
dswbx
2024-12-23 16:50:26 +01:00
parent a17fd2df67
commit 70e42a02d7
31 changed files with 319 additions and 35 deletions

View File

@@ -0,0 +1,37 @@
import { describe, expect, test } from "bun:test";
import * as large from "../../src/media/storage/mime-types";
import * as tiny from "../../src/media/storage/mime-types-tiny";
describe("media/mime-types", () => {
test("tiny resolves", () => {
const tests = [[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"]];
for (const [ext, mime] of tests) {
expect(tiny.guess(ext)).toBe(mime);
}
});
test("all tiny resolves to large", () => {
for (const [ext, mime] of Object.entries(tiny.M)) {
expect(large.guessMimeType("." + ext)).toBe(mime);
}
for (const [type, exts] of Object.entries(tiny.Q)) {
for (const ext of exts) {
const ex = `${type}/${ext}`;
try {
expect(large.guessMimeType("." + ext)).toBe(ex);
} catch (e) {
console.log(`Failed for ${ext}`, {
type,
exts,
ext,
expected: ex,
actual: large.guessMimeType("." + ext)
});
throw new Error(`Failed for ${ext}`);
}
}
}
});
});

View File

@@ -128,6 +128,14 @@ export class Api {
};
}
async getVerifiedAuthState(force?: boolean): Promise<AuthState> {
if (force === true || !this.verified) {
await this.verifyAuth();
}
return this.getAuthState();
}
async verifyAuth() {
try {
const res = await this.auth.me();

View File

@@ -18,11 +18,11 @@ export function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
}
let app: App;
export function serve(config: CreateAppConfig) {
export function serve(config: CreateAppConfig & { beforeBuild?: (app: App) => Promise<void> }) {
return async (args: TAstro) => {
if (!app) {
app = App.create(config);
await config.beforeBuild?.(app);
await app.build();
}
return app.fetch(args.request);

View File

@@ -1,9 +1,10 @@
/// <reference types="bun-types" />
import path from "node:path";
import { App, type CreateAppConfig } from "bknd";
import { App, type CreateAppConfig, registries } from "bknd";
import type { Serve, ServeOptions } from "bun";
import { serveStatic } from "hono/bun";
import { registerLocalMediaAdapter } from "../index";
let app: App;
export type ExtendedAppCreateConfig = Partial<CreateAppConfig> & {
@@ -18,6 +19,7 @@ export async function createApp({
buildOptions,
...config
}: ExtendedAppCreateConfig) {
registerLocalMediaAdapter();
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
if (!app) {

View File

@@ -1,5 +1,6 @@
import type { IncomingMessage } from "node:http";
import type { App, CreateAppConfig } from "bknd";
import { type App, type CreateAppConfig, registries } from "bknd";
import { StorageLocalAdapter } from "media/storage/adapters/StorageLocalAdapter";
export type CloudflareBkndConfig<Env = any> = {
mode?: "warm" | "fresh" | "cache" | "durable";
@@ -47,3 +48,7 @@ export function nodeRequestToRequest(req: IncomingMessage): Request {
headers
});
}
export function registerLocalMediaAdapter() {
registries.media.register("local", StorageLocalAdapter);
}

View File

@@ -18,7 +18,6 @@ type GetServerSidePropsContext = {
export function createApi({ req }: GetServerSidePropsContext) {
const request = nodeRequestToRequest(req);
//console.log("createApi:request.headers", request.headers);
return new Api({
host: new URL(request.url).origin,
headers: request.headers
@@ -43,10 +42,11 @@ function getCleanRequest(req: Request) {
}
let app: App;
export function serve(config: CreateAppConfig) {
export function serve(config: CreateAppConfig & { beforeBuild?: (app: App) => Promise<void> }) {
return async (req: Request) => {
if (!app) {
app = App.create(config);
await config.beforeBuild?.(app);
await app.build();
}
const request = getCleanRequest(req);

View File

@@ -3,3 +3,4 @@ export {
StorageLocalAdapter,
type LocalAdapterConfig
} from "../../media/storage/adapters/StorageLocalAdapter";
export { registerLocalMediaAdapter } from "../index";

View File

@@ -1,7 +1,8 @@
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";
import { App, type CreateAppConfig, registries } from "bknd";
import { registerLocalMediaAdapter } from "../index";
export type NodeAdapterOptions = CreateAppConfig & {
relativeDistPath?: string;
@@ -21,6 +22,8 @@ export function serve({
buildOptions = {},
...config
}: NodeAdapterOptions = {}) {
registerLocalMediaAdapter();
const root = path.relative(
process.cwd(),
path.resolve(relativeDistPath ?? "./node_modules/bknd/dist", "static")

View File

@@ -1,10 +1,11 @@
import { App, type CreateAppConfig } from "bknd";
let app: App;
export function serve(config: CreateAppConfig) {
export function serve(config: CreateAppConfig & { beforeBuild?: (app: App) => Promise<void> }) {
return async (args: { request: Request }) => {
if (!app) {
app = App.create(config);
await config.beforeBuild?.(app);
await app.build();
}
return app.fetch(args.request);

View File

@@ -1,4 +1,5 @@
import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth";
import type { PasswordStrategy } from "auth/authenticate/strategies";
import { Exception } from "core";
import { type Static, secureRandomString, transformObject } from "core/utils";
import { type Entity, EntityIndex, type EntityManager } from "data";
@@ -284,6 +285,21 @@ export class AppAuth extends Module<typeof authConfigSchema> {
} catch (e) {}
}
async createUser(input: { email: string; password: string }) {
const strategy = "password";
const pw = this.authenticator.strategy(strategy) as PasswordStrategy;
const strategy_value = await pw.hash(input.password);
const mutator = this.em.mutator(this.config.entity_name as "users");
mutator.__unstable_toggleSystemEntityCreation(false);
const { data: created } = await mutator.insertOne({
email: input.email,
strategy,
strategy_value
});
mutator.__unstable_toggleSystemEntityCreation(true);
return created;
}
override toJSON(secrets?: boolean): AppAuthSchema {
if (!this.config.enabled) {
return this.configDefault;

View File

@@ -1,7 +1,7 @@
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
import { type Static, Type, parse } from "core/utils";
import type { FileBody, FileListObject, FileMeta, StorageAdapter } from "../../Storage";
import { guessMimeType } from "../../mime-types";
import { guess } from "../../mime-types-tiny";
export const localAdapterConfig = Type.Object(
{
@@ -83,7 +83,7 @@ export class StorageLocalAdapter implements StorageAdapter {
async getObject(key: string, headers: Headers): Promise<Response> {
try {
const content = await readFile(`${this.config.path}/${key}`);
const mimeType = guessMimeType(key);
const mimeType = guess(key);
return new Response(content, {
status: 200,
@@ -105,7 +105,7 @@ export class StorageLocalAdapter implements StorageAdapter {
async getObjectMeta(key: string): Promise<FileMeta> {
const stats = await stat(`${this.config.path}/${key}`);
return {
type: guessMimeType(key) || "application/octet-stream",
type: guess(key) || "application/octet-stream",
size: stats.size
};
}

View File

@@ -0,0 +1,77 @@
export const Q = {
video: ["mp4", "webm"],
audio: ["ogg"],
image: ["jpeg", "png", "gif", "webp", "bmp", "tiff"],
text: ["html", "css", "mdx", "yaml", "vcard", "csv", "vtt"],
application: ["zip", "xml", "toml", "json", "json5"],
font: ["woff", "woff2", "ttf", "otf"]
} as const;
// reduced
const c = {
vnd: "vnd.openxmlformats-officedocument",
z: "application/x-7z-compressed",
t: (w = "plain") => `text/${w}`,
a: (w = "octet-stream") => `application/${w}`,
i: (w) => `image/${w}`,
v: (w) => `video/${w}`
} as const;
export const M = new Map<string, string>([
["7z", c.z],
["7zip", c.z],
["ai", c.a("pdf")],
["apk", c.a("vnd.android.package-archive")],
["doc", c.a("msword")],
["docx", `${c.vnd}.wordprocessingml.document`],
["eps", c.a("postscript")],
["epub", c.a("epub+zip")],
["ini", c.t()],
["jar", c.a("java-archive")],
["jsonld", c.a("ld+json")],
["jpg", c.i("jpeg")],
["log", c.t()],
["m3u", c.t()],
["m3u8", c.a("vnd.apple.mpegurl")],
["manifest", c.t("cache-manifest")],
["md", c.t("markdown")],
["mkv", c.v("x-matroska")],
["mp3", c.a("mpeg")],
["mobi", c.a("x-mobipocket-ebook")],
["ppt", c.a("powerpoint")],
["pptx", `${c.vnd}.presentationml.presentation`],
["qt", c.v("quicktime")],
["svg", c.i("svg+xml")],
["tif", c.i("tiff")],
["tsv", c.t("tab-separated-values")],
["tgz", c.a("x-tar")],
["txt", c.t()],
["text", c.t()],
["vcd", c.a("x-cdlink")],
["vcs", c.t("x-vcalendar")],
["wav", c.a("x-wav")],
["webmanifest", c.a("manifest+json")],
["xls", c.a("vnd.ms-excel")],
["xlsx", `${c.vnd}.spreadsheetml.sheet`],
["yml", c.t("yaml")]
]);
export function guess(f: string): string {
try {
const e = f.split(".").pop() as string;
if (!e) {
return c.a();
}
// try quick first
for (const [t, _e] of Object.entries(Q)) {
// @ts-ignore
if (_e.includes(e)) {
return `${t}/${e}`;
}
}
return M.get(e!) as string;
} catch (e) {
return c.a();
}
}

View File

@@ -63,7 +63,7 @@ const Skeleton = ({ theme = "light" }: { theme?: string }) => {
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
>
<div className="max-h-full flex hover:bg-primary/5 link p-2.5 w-[134px] outline-none">
<Logo />
<Logo theme={theme} />
</div>
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
{[...new Array(5)].map((item, key) => (

View File

@@ -42,7 +42,11 @@ const useLocationFromRouter = (router) => {
];
};
export function Link({ className, ...props }: { className?: string } & LinkProps) {
export function Link({
className,
native,
...props
}: { className?: string; native?: boolean } & LinkProps) {
const router = useRouter();
const [path, navigate] = useLocationFromRouter(router);
@@ -55,8 +59,6 @@ export function Link({ className, ...props }: { className?: string } & LinkProps
return false;
}
function handleClick(e) {}
const _href = props.href ?? props.to;
const href = router
.hrefs(
@@ -72,6 +74,10 @@ export function Link({ className, ...props }: { className?: string } & LinkProps
/*if (active) {
console.log("link", { a, path, absPath, href, to, active, router });
}*/
if (native) {
return <a className={`${active ? "active " : ""}${className}`} {...props} />;
}
return (
// @ts-expect-error className is not typed on WouterLink
<WouterLink className={`${active ? "active " : ""}${className}`} {...props} />

View File

@@ -116,7 +116,7 @@ function SidebarToggler() {
export function Header({ hasSidebar = true }) {
//const logoReturnPath = "";
const { app } = useBknd();
const logoReturnPath = app.getAdminConfig().logo_return_path ?? "/";
const { logo_return_path = "/", color_scheme = "light" } = app.getAdminConfig();
return (
<header
@@ -124,11 +124,11 @@ export function Header({ hasSidebar = true }) {
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
>
<Link
href={logoReturnPath}
replace
href={logo_return_path}
native={logo_return_path !== "/"}
className="max-h-full flex hover:bg-primary/5 link p-2.5 w-[134px] outline-none"
>
<Logo />
<Logo theme={color_scheme} />
</Link>
<HeaderNavigation />
<div className="flex flex-grow" />