Compare commits

..

9 Commits

Author SHA1 Message Date
dswbx 0ff310d6c4 e2e: added script to auto test adapters 2025-04-03 16:38:53 +02:00
dswbx 5178dbee0d e2e: replaced image 2025-04-03 11:07:10 +02:00
dswbx f0f2b571b5 e2e: added adapter configs 2025-04-03 11:04:31 +02:00
dswbx 2cff116fcf Merge remote-tracking branch 'origin/release/0.11' into feat/init-e2e 2025-04-03 09:18:09 +02:00
dswbx 53a48b4b6b e2e: overwrite webserver config with env 2025-04-03 07:56:19 +02:00
dswbx 6f92ef7b74 fix bun picking up e2e tests 2025-04-02 20:50:46 +02:00
dswbx e3628a3dc8 updated/moved vitest, finished merge 2025-04-02 20:39:08 +02:00
dswbx fd4bbccfb7 Merge remote-tracking branch 'origin/release/0.11' into feat/init-e2e
# Conflicts:
#	app/.gitignore
#	app/package.json
#	app/vite.dev.ts
#	bun.lock
2025-04-02 20:24:18 +02:00
dswbx 7ed5db5eaa init e2e 2025-03-28 15:17:04 +01:00
21 changed files with 198 additions and 264 deletions
-1
View File
@@ -1,4 +1,3 @@
playwright-report playwright-report
test-results test-results
bknd.config.* bknd.config.*
__test__/helper.d.ts
+1 -2
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.11.0", "version": "0.11.0-rc.2",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -38,7 +38,6 @@
"test:vitest:watch": "vitest", "test:vitest:watch": "vitest",
"test:vitest:coverage": "vitest run --coverage", "test:vitest:coverage": "vitest run --coverage",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"test:e2e:adapters": "bun run e2e/adapters.ts",
"test:e2e:ui": "playwright test --ui", "test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug", "test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report" "test:e2e:report": "playwright show-report"
+1 -4
View File
@@ -180,10 +180,7 @@ export class App {
registerAdminController(config?: AdminControllerOptions) { registerAdminController(config?: AdminControllerOptions) {
// register admin // register admin
this.adminController = new AdminController(this, config); this.adminController = new AdminController(this, config);
this.modules.server.route( this.modules.server.route(config?.basepath ?? "/", this.adminController.getController());
this.adminController.basepath,
this.adminController.getController(),
);
return this; return this;
} }
+1 -1
View File
@@ -43,7 +43,7 @@ export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
case "url": case "url":
additional.adminOptions = { additional.adminOptions = {
...(typeof adminOptions === "object" ? adminOptions : {}), ...(typeof adminOptions === "object" ? adminOptions : {}),
assetsPath: assets.url, assets_path: assets.url,
}; };
break; break;
default: default:
@@ -1,15 +1,14 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import type { RuntimeBkndConfig } from "bknd/adapter"; import type { FrameworkBkndConfig } from "bknd/adapter";
import { Hono } from "hono"; import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers"; import { serveStatic } from "hono/cloudflare-workers";
import { getFresh } from "./modes/fresh";
import { getCached } from "./modes/cached"; import { getCached } from "./modes/cached";
import { getDurable } from "./modes/durable"; import { getDurable } from "./modes/durable";
import type { App } from "bknd"; import { getFresh, getWarm } from "./modes/fresh";
export type CloudflareEnv = object; export type CloudflareEnv = object;
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & { export type CloudflareBkndConfig<Env = CloudflareEnv> = FrameworkBkndConfig<Env> & {
mode?: "warm" | "fresh" | "cache" | "durable"; mode?: "warm" | "fresh" | "cache" | "durable";
bindings?: (args: Env) => { bindings?: (args: Env) => {
kv?: KVNamespace; kv?: KVNamespace;
@@ -21,6 +20,8 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
keepAliveSeconds?: number; keepAliveSeconds?: number;
forceHttps?: boolean; forceHttps?: boolean;
manifest?: string; manifest?: string;
setAdminHtml?: boolean;
html?: string;
}; };
export type Context<Env = CloudflareEnv> = { export type Context<Env = CloudflareEnv> = {
@@ -42,7 +43,7 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
throw new Error("manifest is required with static 'kv'"); throw new Error("manifest is required with static 'kv'");
} }
if (config.manifest && config.static === "kv") { if (config.manifest && config.static !== "assets") {
const pathname = url.pathname.slice(1); const pathname = url.pathname.slice(1);
const assetManifest = JSON.parse(config.manifest); const assetManifest = JSON.parse(config.manifest);
if (pathname && pathname in assetManifest) { if (pathname && pathname in assetManifest) {
@@ -69,24 +70,18 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
const context = { request, env, ctx } as Context<Env>; const context = { request, env, ctx } as Context<Env>;
const mode = config.mode ?? "warm"; const mode = config.mode ?? "warm";
let app: App;
switch (mode) { switch (mode) {
case "fresh": case "fresh":
app = await getFresh(config, context, { force: true }); return await getFresh(config, context);
break;
case "warm": case "warm":
app = await getFresh(config, context); return await getWarm(config, context);
break;
case "cache": case "cache":
app = await getCached(config, context); return await getCached(config, context);
break;
case "durable": case "durable":
return await getDurable(config, context); return await getDurable(config, context);
default: default:
throw new Error(`Unknown mode ${mode}`); throw new Error(`Unknown mode ${mode}`);
} }
return app.fetch(request, env, ctx);
}, },
}; };
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import { D1Connection, type D1ConnectionConfig } from "./D1Connection"; import { D1Connection, type D1ConnectionConfig } from "./D1Connection";
export * from "./cloudflare-workers.adapter"; export * from "./cloudflare-workers.adapter";
export { makeApp, getFresh } from "./modes/fresh"; export { makeApp, getFresh, getWarm } from "./modes/fresh";
export { getCached } from "./modes/cached"; export { getCached } from "./modes/cached";
export { DurableBkndApp, getDurable } from "./modes/durable"; export { DurableBkndApp, getDurable } from "./modes/durable";
export { D1Connection, type D1ConnectionConfig }; export { D1Connection, type D1ConnectionConfig };
@@ -40,6 +40,7 @@ export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
); );
await config.beforeBuild?.(app); await config.beforeBuild?.(app);
}, },
adminOptions: { html: config.html },
}, },
{ env, ctx, ...args }, { env, ctx, ...args },
); );
+2 -1
View File
@@ -25,7 +25,9 @@ export async function getDurable<Env extends CloudflareEnv = CloudflareEnv>(
const res = await stub.fire(ctx.request, { const res = await stub.fire(ctx.request, {
config: create_config, config: create_config,
html: config.html,
keepAliveSeconds: config.keepAliveSeconds, keepAliveSeconds: config.keepAliveSeconds,
setAdminHtml: config.setAdminHtml,
}); });
const headers = new Headers(res.headers); const headers = new Headers(res.headers);
@@ -108,7 +110,6 @@ export class DurableBkndApp extends DurableObject {
} }
async onBuilt(app: App) {} async onBuilt(app: App) {}
async beforeBuild(app: App) {} async beforeBuild(app: App) {}
protected keepAlive(seconds: number) { protected keepAlive(seconds: number) {
+22 -3
View File
@@ -7,15 +7,22 @@ export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
args: Env = {} as Env, args: Env = {} as Env,
opts?: RuntimeOptions, opts?: RuntimeOptions,
) { ) {
return await createRuntimeApp<Env>(makeConfig(config, args), args, opts); return await createRuntimeApp<Env>(
{
...makeConfig(config, args),
adminOptions: config.html ? { html: config.html } : undefined,
},
args,
opts,
);
} }
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>( export async function getWarm<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>, config: CloudflareBkndConfig<Env>,
ctx: Context<Env>, ctx: Context<Env>,
opts: RuntimeOptions = {}, opts: RuntimeOptions = {},
) { ) {
return await makeApp( const app = await makeApp(
{ {
...config, ...config,
onBuilt: async (app) => { onBuilt: async (app) => {
@@ -26,4 +33,16 @@ export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
ctx.env, ctx.env,
opts, opts,
); );
return app.fetch(ctx.request);
}
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,
ctx: Context<Env>,
opts: RuntimeOptions = {},
) {
return await getWarm(config, ctx, {
...opts,
force: true,
});
} }
+38 -32
View File
@@ -1,24 +1,18 @@
import { serveStatic } from "@hono/node-server/serve-static"; import { serveStatic } from "@hono/node-server/serve-static";
import { import { type DevServerOptions, default as honoViteDevServer } from "@hono/vite-dev-server";
type DevServerOptions,
default as honoViteDevServer,
} from "@hono/vite-dev-server";
import type { App } from "bknd"; import type { App } from "bknd";
import { import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
type RuntimeBkndConfig,
createRuntimeApp,
type FrameworkOptions,
} from "bknd/adapter";
import { registerLocalMediaAdapter } from "bknd/adapter/node"; import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { devServerConfig } from "./dev-server-config"; import { devServerConfig } from "./dev-server-config";
export type ViteEnv = NodeJS.ProcessEnv; export type ViteBkndConfig<Env = any> = RuntimeBkndConfig<Env> & {
export type ViteBkndConfig<Env = ViteEnv> = RuntimeBkndConfig<Env> & {}; mode?: "cached" | "fresh";
setAdminHtml?: boolean;
forceDev?: boolean | { mainPath: string };
html?: string;
};
export function addViteScript( export function addViteScript(html: string, addBkndContext: boolean = true) {
html: string,
addBkndContext: boolean = true,
) {
return html.replace( return html.replace(
"</head>", "</head>",
`<script type="module"> `<script type="module">
@@ -34,40 +28,52 @@ ${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
); );
} }
async function createApp<ViteEnv>( async function createApp(config: ViteBkndConfig = {}, env?: any) {
config: ViteBkndConfig<ViteEnv> = {},
env: ViteEnv = {} as ViteEnv,
opts: FrameworkOptions = {},
): Promise<App> {
registerLocalMediaAdapter(); registerLocalMediaAdapter();
return await createRuntimeApp( return await createRuntimeApp(
{ {
...config, ...config,
adminOptions: config.adminOptions ?? { adminOptions:
forceDev: { config.setAdminHtml === false
mainPath: "/src/main.tsx", ? undefined
}, : {
}, html: config.html,
forceDev: config.forceDev ?? {
mainPath: "/src/main.tsx",
},
},
serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })], serveStatic: ["/assets/*", serveStatic({ root: config.distPath ?? "./" })],
}, },
env, env,
opts,
); );
} }
export function serve<ViteEnv>( export function serveFresh(config: Omit<ViteBkndConfig, "mode"> = {}) {
config: ViteBkndConfig<ViteEnv> = {},
args?: ViteEnv,
opts?: FrameworkOptions,
) {
return { return {
async fetch(request: Request, env: any, ctx: ExecutionContext) { async fetch(request: Request, env: any, ctx: ExecutionContext) {
const app = await createApp(config, env, opts); const app = await createApp(config, env);
return app.fetch(request, env, ctx); return app.fetch(request, env, ctx);
}, },
}; };
} }
let app: App;
export function serveCached(config: Omit<ViteBkndConfig, "mode"> = {}) {
return {
async fetch(request: Request, env: any, ctx: ExecutionContext) {
if (!app) {
app = await createApp(config, env);
}
return app.fetch(request, env, ctx);
},
};
}
export function serve({ mode, ...config }: ViteBkndConfig = {}) {
return mode === "fresh" ? serveFresh(config) : serveCached(config);
}
export function devServer(options: DevServerOptions) { export function devServer(options: DevServerOptions) {
return honoViteDevServer({ return honoViteDevServer({
...devServerConfig, ...devServerConfig,
+11 -6
View File
@@ -1,4 +1,4 @@
import { type DB, Exception, type PrimaryFieldType } from "core"; import { type DB, Exception } from "core";
import { addFlashMessage } from "core/server/flash"; import { addFlashMessage } from "core/server/flash";
import { import {
type Static, type Static,
@@ -14,7 +14,6 @@ import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
import { sign, verify } from "hono/jwt"; import { sign, verify } from "hono/jwt";
import type { CookieOptions } from "hono/utils/cookie"; import type { CookieOptions } from "hono/utils/cookie";
import type { ServerEnv } from "modules/Controller"; import type { ServerEnv } from "modules/Controller";
import { pick } from "lodash-es";
type Input = any; // workaround type Input = any; // workaround
export type JWTPayload = Parameters<typeof sign>[0]; export type JWTPayload = Parameters<typeof sign>[0];
@@ -38,10 +37,11 @@ export interface Strategy {
} }
export type User = { export type User = {
id: PrimaryFieldType; id: number;
email: string; email: string;
username: string;
password: string; password: string;
role?: string | null; role: string;
}; };
export type ProfileExchange = { export type ProfileExchange = {
@@ -158,8 +158,13 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
} }
// @todo: add jwt tests // @todo: add jwt tests
async jwt(_user: Omit<User, "password">): Promise<string> { async jwt(user: Omit<User, "password">): Promise<string> {
const user = pick(_user, this.config.jwt.fields); const prohibited = ["password"];
for (const prop of prohibited) {
if (prop in user) {
throw new Error(`Property "${prop}" is prohibited`);
}
}
const payload: JWTPayload = { const payload: JWTPayload = {
...user, ...user,
@@ -29,15 +29,13 @@ export const cloudflare = {
{ dir: ctx.dir }, { dir: ctx.dir },
); );
const db = ctx.skip const db = ctx.skip ? "d1" : await $p.select({
? "d1" message: "What database do you want to use?",
: await $p.select({ options: [
message: "What database do you want to use?", { label: "Cloudflare D1", value: "d1" },
options: [ { label: "LibSQL", value: "libsql" },
{ label: "Cloudflare D1", value: "d1" }, ],
{ label: "LibSQL", value: "libsql" }, });
],
});
if ($p.isCancel(db)) { if ($p.isCancel(db)) {
process.exit(1); process.exit(1);
} }
@@ -66,19 +64,17 @@ export const cloudflare = {
async function createD1(ctx: TemplateSetupCtx) { async function createD1(ctx: TemplateSetupCtx) {
const default_db = "data"; const default_db = "data";
const name = ctx.skip const name = ctx.skip ? default_db : await $p.text({
? default_db message: "Enter database name",
: await $p.text({ initialValue: default_db,
message: "Enter database name", placeholder: default_db,
initialValue: default_db, validate: (v) => {
placeholder: default_db, if (!v) {
validate: (v) => { return "Invalid name";
if (!v) { }
return "Invalid name"; return;
} },
return; });
},
});
if ($p.isCancel(name)) { if ($p.isCancel(name)) {
process.exit(1); process.exit(1);
} }
@@ -157,16 +153,13 @@ async function createLibsql(ctx: TemplateSetupCtx) {
} }
async function createR2(ctx: TemplateSetupCtx) { async function createR2(ctx: TemplateSetupCtx) {
const create = ctx.skip const create = ctx.skip ?? await $p.confirm({
? false message: "Do you want to use a R2 bucket?",
: await $p.confirm({ initialValue: true,
message: "Do you want to use a R2 bucket?", });
initialValue: true,
});
if ($p.isCancel(create)) { if ($p.isCancel(create)) {
process.exit(1); process.exit(1);
} }
if (!create) { if (!create) {
await overrideJson( await overrideJson(
WRANGLER_FILE, WRANGLER_FILE,
@@ -180,19 +173,17 @@ async function createR2(ctx: TemplateSetupCtx) {
} }
const default_bucket = "bucket"; const default_bucket = "bucket";
const name = ctx.skip const name = ctx.skip ? default_bucket : await $p.text({
? default_bucket message: "Enter bucket name",
: await $p.text({ initialValue: default_bucket,
message: "Enter bucket name", placeholder: default_bucket,
initialValue: default_bucket, validate: (v) => {
placeholder: default_bucket, if (!v) {
validate: (v) => { return "Invalid name";
if (!v) { }
return "Invalid name"; return;
} },
return; });
},
});
if ($p.isCancel(name)) { if ($p.isCancel(name)) {
process.exit(1); process.exit(1);
} }
+5 -14
View File
@@ -17,13 +17,12 @@ import {
startServer, startServer,
} from "./platform"; } from "./platform";
import { makeConfig } from "adapter"; import { makeConfig } from "adapter";
import { isBun as $isBun } from "cli/utils/sys";
const env_files = [".env", ".dev.vars"]; const env_files = [".env", ".dev.vars"];
dotenv.config({ dotenv.config({
path: env_files.map((file) => path.resolve(process.cwd(), file)), path: env_files.map((file) => path.resolve(process.cwd(), file)),
}); });
const isBun = $isBun(); const isBun = typeof Bun !== "undefined";
export const run: CliCommand = (program) => { export const run: CliCommand = (program) => {
program program
@@ -99,7 +98,7 @@ export async function makeConfigApp(_config: CliBkndConfig, platform?: Platform)
}); });
} }
type RunOptions = { async function action(options: {
port: number; port: number;
memory?: boolean; memory?: boolean;
config?: string; config?: string;
@@ -107,9 +106,8 @@ type RunOptions = {
dbToken?: string; dbToken?: string;
server: Platform; server: Platform;
open?: boolean; open?: boolean;
}; }) {
colorizeConsole(console);
export async function makeAppFromEnv(options: Partial<RunOptions> = {}) {
const configFilePath = await getConfigPath(options.config); const configFilePath = await getConfigPath(options.config);
let app: App | undefined = undefined; let app: App | undefined = undefined;
@@ -149,19 +147,12 @@ export async function makeAppFromEnv(options: Partial<RunOptions> = {}) {
// if nothing helps, create a file based app // if nothing helps, create a file based app
if (!app) { if (!app) {
const connection = { url: "file:data.db" } as Config; const connection = { url: "file:data.db" } as Config;
console.info("Using fallback connection", c.cyan(connection.url)); console.info("Using connection", c.cyan(connection.url));
app = await makeApp({ app = await makeApp({
connection, connection,
server: { platform: options.server }, server: { platform: options.server },
}); });
} }
return app;
}
async function action(options: RunOptions) {
colorizeConsole(console);
const app = await makeAppFromEnv(options);
await startServer(options.server, app, { port: options.port, open: options.open }); await startServer(options.server, app, { port: options.port, open: options.open });
} }
+36 -70
View File
@@ -1,32 +1,28 @@
import { import { password as $password, text as $text } from "@clack/prompts";
isCancel as $isCancel,
log as $log,
password as $password,
text as $text,
} from "@clack/prompts";
import type { App } from "App"; import type { App } from "App";
import type { PasswordStrategy } from "auth/authenticate/strategies"; import type { PasswordStrategy } from "auth/authenticate/strategies";
import { makeAppFromEnv } from "cli/commands/run"; import { makeConfigApp } from "cli/commands/run";
import type { CliCommand } from "cli/types"; import { getConfigPath } from "cli/commands/run/platform";
import type { CliBkndConfig, CliCommand } from "cli/types";
import { Argument } from "commander"; import { Argument } from "commander";
import { $console } from "core";
import c from "picocolors";
import { isBun } from "cli/utils/sys";
export const user: CliCommand = (program) => { export const user: CliCommand = (program) => {
program program
.command("user") .command("user")
.description("create/update users, or generate a token (auth)") .description("create and update user (auth)")
.addArgument( .addArgument(new Argument("<action>", "action to perform").choices(["create", "update"]))
new Argument("<action>", "action to perform").choices(["create", "update", "token"]),
)
.action(action); .action(action);
}; };
async function action(action: "create" | "update" | "token", options: any) { async function action(action: "create" | "update", options: any) {
const app = await makeAppFromEnv({ const configFilePath = await getConfigPath();
server: "node", if (!configFilePath) {
}); console.error("config file not found");
return;
}
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
const app = await makeConfigApp(config, options.server);
switch (action) { switch (action) {
case "create": case "create":
@@ -35,9 +31,6 @@ async function action(action: "create" | "update" | "token", options: any) {
case "update": case "update":
await update(app, options); await update(app, options);
break; break;
case "token":
await token(app, options);
break;
} }
} }
@@ -45,8 +38,7 @@ async function create(app: App, options: any) {
const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy; const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy;
if (!strategy) { if (!strategy) {
$log.error("Password strategy not configured"); throw new Error("Password strategy not configured");
process.exit(1);
} }
const email = await $text({ const email = await $text({
@@ -58,7 +50,6 @@ async function create(app: App, options: any) {
return; return;
}, },
}); });
if ($isCancel(email)) process.exit(1);
const password = await $password({ const password = await $password({
message: "Enter password", message: "Enter password",
@@ -69,17 +60,20 @@ async function create(app: App, options: any) {
return; return;
}, },
}); });
if ($isCancel(password)) process.exit(1);
if (typeof email !== "string" || typeof password !== "string") {
console.log("Cancelled");
process.exit(0);
}
try { try {
const created = await app.createUser({ const created = await app.createUser({
email, email,
password: await strategy.hash(password as string), password: await strategy.hash(password as string),
}); });
$log.success(`Created user: ${c.cyan(created.email)}`); console.log("Created:", created);
} catch (e) { } catch (e) {
$log.error("Error creating user"); console.error("Error", e);
$console.error(e);
} }
} }
@@ -98,14 +92,17 @@ async function update(app: App, options: any) {
return; return;
}, },
})) as string; })) as string;
if ($isCancel(email)) process.exit(1); if (typeof email !== "string") {
console.log("Cancelled");
process.exit(0);
}
const { data: user } = await em.repository(users_entity).findOne({ email }); const { data: user } = await em.repository(users_entity).findOne({ email });
if (!user) { if (!user) {
$log.error("User not found"); console.log("User not found");
process.exit(1); process.exit(0);
} }
$log.info(`User found: ${c.cyan(user.email)}`); console.log("User found:", user);
const password = await $password({ const password = await $password({
message: "New Password?", message: "New Password?",
@@ -116,7 +113,10 @@ async function update(app: App, options: any) {
return; return;
}, },
}); });
if ($isCancel(password)) process.exit(1); if (typeof password !== "string") {
console.log("Cancelled");
process.exit(0);
}
try { try {
function togglePw(visible: boolean) { function togglePw(visible: boolean) {
@@ -134,42 +134,8 @@ async function update(app: App, options: any) {
}); });
togglePw(false); togglePw(false);
$log.success(`Updated user: ${c.cyan(user.email)}`); console.log("Updated:", user);
} catch (e) { } catch (e) {
$log.error("Error updating user"); console.error("Error", e);
$console.error(e);
} }
} }
async function token(app: App, options: any) {
if (isBun()) {
$log.error("Please use node to generate tokens");
process.exit(1);
}
const config = app.module.auth.toJSON(true);
const users_entity = config.entity_name as "users";
const em = app.modules.ctx().em;
const email = (await $text({
message: "Which user? Enter email",
validate: (v) => {
if (!v.includes("@")) {
return "Invalid email";
}
return;
},
})) as string;
if ($isCancel(email)) process.exit(1);
const { data: user } = await em.repository(users_entity).findOne({ email });
if (!user) {
$log.error("User not found");
process.exit(1);
}
$log.info(`User found: ${c.cyan(user.email)}`);
console.log(
`\n${c.dim("Token:")}\n${c.yellow(await app.module.auth.authenticator.jwt(user))}\n`,
);
}
-8
View File
@@ -3,14 +3,6 @@ import { readFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import url from "node:url"; import url from "node:url";
export function isBun(): boolean {
try {
return typeof Bun !== "undefined";
} catch (e) {
return false;
}
}
export function getRootPath() { export function getRootPath() {
const _path = path.dirname(url.fileURLToPath(import.meta.url)); const _path = path.dirname(url.fileURLToPath(import.meta.url));
// because of "src", local needs one more level up // because of "src", local needs one more level up
+1 -2
View File
@@ -1,7 +1,6 @@
import type { App } from "App"; import type { App } from "App";
import { type Context, Hono } from "hono"; import { type Context, Hono } from "hono";
import * as middlewares from "modules/middlewares"; import * as middlewares from "modules/middlewares";
import type { SafeUser } from "auth";
export type ServerEnv = { export type ServerEnv = {
Variables: { Variables: {
@@ -11,7 +10,7 @@ export type ServerEnv = {
resolved: boolean; resolved: boolean;
registered: boolean; registered: boolean;
skip: boolean; skip: boolean;
user?: SafeUser; user?: { id: any; role?: string; [key: string]: any };
}; };
html?: string; html?: string;
}; };
+14 -20
View File
@@ -14,11 +14,10 @@ const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
// @todo: add migration to remove admin path from config // @todo: add migration to remove admin path from config
export type AdminControllerOptions = { export type AdminControllerOptions = {
basepath?: string; basepath?: string;
adminBasepath?: string; assets_path?: string;
assetsPath?: string;
html?: string; html?: string;
forceDev?: boolean | { mainPath: string }; forceDev?: boolean | { mainPath: string };
debugRerenders?: boolean; debug_rerenders?: boolean;
}; };
export class AdminController extends Controller { export class AdminController extends Controller {
@@ -37,8 +36,7 @@ export class AdminController extends Controller {
return { return {
...this._options, ...this._options,
basepath: this._options.basepath ?? "/", basepath: this._options.basepath ?? "/",
adminBasepath: this._options.adminBasepath ?? "", assets_path: this._options.assets_path ?? config.server.assets_path,
assetsPath: this._options.assetsPath ?? config.server.assets_path,
}; };
} }
@@ -50,10 +48,6 @@ export class AdminController extends Controller {
return (this.basepath + route).replace(/(?<!:)\/+/g, "/"); return (this.basepath + route).replace(/(?<!:)\/+/g, "/");
} }
private withAdminBasePath(route: string = "") {
return this.withBasePath(this.options.adminBasepath + route);
}
override getController() { override getController() {
const { auth: authMiddleware, permission } = this.middlewares; const { auth: authMiddleware, permission } = this.middlewares;
const hono = this.create().use( const hono = this.create().use(
@@ -69,16 +63,16 @@ export class AdminController extends Controller {
const authRoutes = { const authRoutes = {
root: "/", root: "/",
success: configs.auth.cookie.pathSuccess ?? this.withAdminBasePath("/"), success: configs.auth.cookie.pathSuccess ?? "/",
loggedOut: configs.auth.cookie.pathLoggedOut ?? this.withAdminBasePath("/"), loggedOut: configs.auth.cookie.pathLoggedOut ?? "/",
login: this.withAdminBasePath("/auth/login"), login: "/auth/login",
logout: this.withAdminBasePath("/auth/logout"), logout: "/auth/logout",
}; };
hono.use("*", async (c, next) => { hono.use("*", async (c, next) => {
const obj = { const obj = {
user: c.get("auth")?.user, user: c.get("auth")?.user,
logout_route: this.withAdminBasePath(authRoutes.logout), logout_route: this.withBasePath(authRoutes.logout),
}; };
const html = await this.getHtml(obj); const html = await this.getHtml(obj);
if (!html) { if (!html) {
@@ -170,8 +164,8 @@ export class AdminController extends Controller {
if (isProd) { if (isProd) {
let manifest: any; let manifest: any;
if (this.options.assetsPath.startsWith("http")) { if (this.options.assets_path.startsWith("http")) {
manifest = await fetch(this.options.assetsPath + "manifest.json", { manifest = await fetch(this.options.assets_path + "manifest.json", {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
}, },
@@ -188,7 +182,7 @@ export class AdminController extends Controller {
assets.css = manifest["src/ui/main.tsx"].css[0] as any; assets.css = manifest["src/ui/main.tsx"].css[0] as any;
} }
const favicon = isProd ? this.options.assetsPath + "favicon.ico" : "/favicon.ico"; const favicon = isProd ? this.options.assets_path + "favicon.ico" : "/favicon.ico";
return ( return (
<Fragment> <Fragment>
@@ -203,7 +197,7 @@ export class AdminController extends Controller {
/> />
<link rel="icon" href={favicon} type="image/x-icon" /> <link rel="icon" href={favicon} type="image/x-icon" />
<title>BKND</title> <title>BKND</title>
{this.options.debugRerenders && ( {this.options.debug_rerenders && (
<script <script
crossOrigin="anonymous" crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js" src="//unpkg.com/react-scan/dist/auto.global.js"
@@ -211,8 +205,8 @@ export class AdminController extends Controller {
)} )}
{isProd ? ( {isProd ? (
<Fragment> <Fragment>
<script type="module" src={this.options.assetsPath + assets?.js} /> <script type="module" src={this.options.assets_path + assets?.js} />
<link rel="stylesheet" href={this.options.assetsPath + assets?.css} /> <link rel="stylesheet" href={this.options.assets_path + assets?.css} />
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Fragment>
+2 -9
View File
@@ -101,21 +101,14 @@ export function BkndProvider({
fallback: true, fallback: true,
} as any); } as any);
startTransition(() => { startTransition(() => {
const commit = () => { document.startViewTransition(() => {
setSchema(newSchema); setSchema(newSchema);
setWithSecrets(_includeSecrets); setWithSecrets(_includeSecrets);
setFetched(true); setFetched(true);
set_local_version((v) => v + 1); set_local_version((v) => v + 1);
fetching.current = Fetching.None; fetching.current = Fetching.None;
} });
if ("startViewTransition" in document) {
document.startViewTransition(commit);
} else {
commit();
}
}); });
} }
+17 -29
View File
@@ -11,7 +11,7 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { useApi, useApiInfiniteQuery, useApiQuery, useInvalidate } from "ui/client"; import { useApi, useApiInfiniteQuery, useInvalidate } from "ui/client";
import { useEvent } from "ui/hooks/use-event"; import { useEvent } from "ui/hooks/use-event";
import { Dropzone, type DropzoneProps, type DropzoneRenderProps, type FileState } from "./Dropzone"; import { Dropzone, type DropzoneProps, type DropzoneRenderProps, type FileState } from "./Dropzone";
import { mediaItemsToFileStates } from "./helper"; import { mediaItemsToFileStates } from "./helper";
@@ -20,7 +20,6 @@ import { useInViewport } from "@mantine/hooks";
export type DropzoneContainerProps = { export type DropzoneContainerProps = {
children?: ReactNode; children?: ReactNode;
initialItems?: MediaFieldSchema[] | false; initialItems?: MediaFieldSchema[] | false;
infinite?: boolean;
entity?: { entity?: {
name: string; name: string;
id: number; id: number;
@@ -40,7 +39,6 @@ export function DropzoneContainer({
query, query,
children, children,
randomFilename, randomFilename,
infinite = false,
...props ...props
}: DropzoneContainerProps) { }: DropzoneContainerProps) {
const id = useId(); const id = useId();
@@ -56,7 +54,7 @@ export function DropzoneContainer({
const entity_name = (media?.entity_name ?? "media") as "media"; const entity_name = (media?.entity_name ?? "media") as "media";
//console.log("dropzone:baseUrl", baseUrl); //console.log("dropzone:baseUrl", baseUrl);
const selectApi = (api: Api, page: number = 0) => const selectApi = (api: Api, page: number) =>
entity entity
? api.data.readManyByReference(entity.name, entity.id, entity.field, { ? api.data.readManyByReference(entity.name, entity.id, entity.field, {
...query, ...query,
@@ -72,11 +70,7 @@ export function DropzoneContainer({
...defaultQuery(page), ...defaultQuery(page),
}); });
const $q = infinite const $q = useApiInfiniteQuery(selectApi, {});
? useApiInfiniteQuery(selectApi, {})
: useApiQuery(selectApi, {
enabled: initialItems !== false && !initialItems,
});
const getUploadInfo = useEvent((file) => { const getUploadInfo = useEvent((file) => {
const url = entity const url = entity
@@ -114,17 +108,11 @@ export function DropzoneContainer({
autoUpload autoUpload
initialItems={_initialItems} initialItems={_initialItems}
footer={ footer={
infinite && <Footer
"setSize" in $q && ( items={_initialItems.length}
<Footer length={$q._data?.[0]?.body.meta.count ?? 0}
items={_initialItems.length} onFirstVisible={() => $q.setSize($q.size + 1)}
length={Math.min( />
$q._data?.[0]?.body.meta.count ?? 0,
_initialItems.length + pageSize,
)}
onFirstVisible={() => $q.setSize($q.size + 1)}
/>
)
} }
{...props} {...props}
> >
@@ -154,15 +142,15 @@ const Footer = ({ items = 0, length = 0, onFirstVisible }) => {
const _len = length - items; const _len = length - items;
if (_len <= 0) return null; if (_len <= 0) return null;
return new Array(Math.max(length - items, 0)) return new Array(Math.max(length - items, 0)).fill(0).map((_, i) => (
.fill(0) <div
.map((_, i) => ( key={i}
<div ref={i === 0 ? ref : undefined}
key={i} className="w-[49%] md:w-60 bg-muted aspect-square"
ref={i === 0 ? ref : undefined} >
className="w-[49%] md:w-60 bg-muted aspect-square" {i === 0 ? (inViewport ? `load ${visible}` : "first") : "other"}
/> </div>
)); ));
}; };
export function useDropzone() { export function useDropzone() {
+1 -1
View File
@@ -35,7 +35,7 @@ export function MediaIndex() {
return ( return (
<AppShell.Scrollable> <AppShell.Scrollable>
<div className="flex flex-1 p-3"> <div className="flex flex-1 p-3">
<Media.Dropzone onClick={onClick} infinite /> <Media.Dropzone onClick={onClick} />
</div> </div>
</AppShell.Scrollable> </AppShell.Scrollable>
); );
+1 -3
View File
@@ -66,9 +66,7 @@ export default {
}, },
"sync", "sync",
); );
await app.build({ await app.build();
sync: !!(firstStart && example),
});
// log routes // log routes
if (firstStart) { if (firstStart) {