Merge branch 'release/0.8' into refactor/data-api-entity-prefix

This commit is contained in:
dswbx
2025-02-18 10:22:15 +01:00
committed by GitHub
20 changed files with 166 additions and 78 deletions

View File

@@ -26,6 +26,10 @@ export class AppFirstBoot extends AppEvent {
}
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
export type AppOptions = {
plugins?: AppPlugin[];
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated">;
};
export type CreateAppConfig = {
connection?:
| Connection
@@ -36,8 +40,7 @@ export type CreateAppConfig = {
}
| LibSqlCredentials;
initialConfig?: InitialModuleConfigs;
plugins?: AppPlugin[];
options?: Omit<ModuleManagerOptions, "initial" | "onUpdated">;
options?: AppOptions;
};
export type AppConfig = InitialModuleConfigs;
@@ -47,32 +50,33 @@ export class App {
static readonly Events = AppEvents;
adminController?: AdminController;
private trigger_first_boot = false;
private plugins: AppPlugin[];
constructor(
private connection: Connection,
_initialConfig?: InitialModuleConfigs,
private plugins: AppPlugin[] = [],
moduleManagerOptions?: ModuleManagerOptions
private options?: AppOptions
) {
this.plugins = options?.plugins ?? [];
this.modules = new ModuleManager(connection, {
...moduleManagerOptions,
...(options?.manager ?? {}),
initial: _initialConfig,
onUpdated: async (key, config) => {
// if the EventManager was disabled, we assume we shouldn't
// respond to events, such as "onUpdated".
// this is important if multiple changes are done, and then build() is called manually
if (!this.emgr.enabled) {
console.warn("[APP] config updated, but event manager is disabled, skip.");
console.warn("App config updated, but event manager is disabled, skip.");
return;
}
console.log("[APP] config updated", key);
console.log("App config updated", key);
// @todo: potentially double syncing
await this.build({ sync: true });
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
},
onFirstBoot: async () => {
console.log("[APP] first boot");
console.log("App first boot");
this.trigger_first_boot = true;
},
onServerInit: async (server) => {
@@ -106,8 +110,6 @@ export class App {
await this.emgr.emit(new AppBuiltEvent({ app: this }));
server.all("/api/*", async (c) => c.notFound());
// first boot is set from ModuleManager when there wasn't a config table
if (this.trigger_first_boot) {
this.trigger_first_boot = false;
@@ -183,7 +185,7 @@ export function createApp(config: CreateAppConfig = {}) {
} else if (typeof config.connection === "object") {
if ("type" in config.connection) {
console.warn(
"[WARN] Using deprecated connection type 'libsql', use the 'config' object directly."
"Using deprecated connection type 'libsql', use the 'config' object directly."
);
connection = new LibsqlConnection(config.connection.config);
} else {
@@ -191,7 +193,7 @@ export function createApp(config: CreateAppConfig = {}) {
}
} else {
connection = new LibsqlConnection({ url: ":memory:" });
console.warn("[!] No connection provided, using in-memory database");
console.warn("No connection provided, using in-memory database");
}
} catch (e) {
console.error("Could not create connection", e);
@@ -201,5 +203,5 @@ export function createApp(config: CreateAppConfig = {}) {
throw new Error("Invalid connection");
}
return new App(connection, config.initialConfig, config.plugins, config.options);
return new App(connection, config.initialConfig, config.options);
}

View File

@@ -30,7 +30,6 @@ export function serve({
distPath,
connection,
initialConfig,
plugins,
options,
port = config.server.default_port,
onBuilt,
@@ -44,7 +43,6 @@ export function serve({
const app = await createApp({
connection,
initialConfig,
plugins,
options,
onBuilt,
buildConfig,

View File

@@ -1,20 +1,45 @@
import path from "node:path";
import url from "node:url";
import { createApp } from "App";
import { getConnectionCredentialsFromEnv } from "cli/commands/run/platform";
import { getDistPath, getRelativeDistPath, getRootPath } from "cli/utils/sys";
import { Argument } from "commander";
import { showRoutes } from "hono/dev";
import type { CliCommand } from "../types";
export const debug: CliCommand = (program) => {
program
.command("debug")
.description("debug path resolution")
.action(() => {
console.log("paths", {
rootpath: getRootPath(),
distPath: getDistPath(),
relativeDistPath: getRelativeDistPath(),
cwd: process.cwd(),
dir: path.dirname(url.fileURLToPath(import.meta.url)),
resolvedPkg: path.resolve(getRootPath(), "package.json")
});
});
.description("debug bknd")
.addArgument(new Argument("<subject>", "subject to debug").choices(Object.keys(subjects)))
.action(action);
};
const subjects = {
paths: async () => {
console.log("[PATHS]", {
rootpath: getRootPath(),
distPath: getDistPath(),
relativeDistPath: getRelativeDistPath(),
cwd: process.cwd(),
dir: path.dirname(url.fileURLToPath(import.meta.url)),
resolvedPkg: path.resolve(getRootPath(), "package.json")
});
},
routes: async () => {
console.log("[APP ROUTES]");
const credentials = getConnectionCredentialsFromEnv();
const app = createApp({ connection: credentials });
await app.build();
showRoutes(app.server);
}
};
async function action(subject: string) {
console.log("debug", { subject });
if (subject in subjects) {
await subjects[subject]();
} else {
console.error("Invalid subject: ", subject);
}
}

View File

@@ -32,7 +32,7 @@ export async function attachServeStatic(app: any, platform: Platform) {
export async function startServer(server: Platform, app: any, options: { port: number }) {
const port = options.port;
console.log(`(using ${server} serve)`);
console.log(`Using ${server} serve`);
switch (server) {
case "node": {
@@ -54,7 +54,7 @@ export async function startServer(server: Platform, app: any, options: { port: n
}
const url = `http://localhost:${port}`;
console.log(`Server listening on ${url}`);
console.info("Server listening on", url);
await open(url);
}

View File

@@ -2,10 +2,12 @@ import type { Config } from "@libsql/client/node";
import { App, type CreateAppConfig } from "App";
import { StorageLocalAdapter } from "adapter/node";
import type { CliBkndConfig, CliCommand } from "cli/types";
import { replaceConsole } from "cli/utils/cli";
import { Option } from "commander";
import { config } from "core";
import dotenv from "dotenv";
import { registries } from "modules/registries";
import c from "picocolors";
import {
PLATFORMS,
type Platform,
@@ -27,6 +29,13 @@ export const run: CliCommand = (program) => {
.default(config.server.default_port)
.argParser((v) => Number.parseInt(v))
)
.addOption(
new Option("-m, --memory", "use in-memory database").conflicts([
"config",
"db-url",
"db-token"
])
)
.addOption(new Option("-c, --config <config>", "config file"))
.addOption(
new Option("--db-url <db>", "database url, can be any valid libsql url").conflicts(
@@ -97,33 +106,44 @@ export async function makeConfigApp(config: CliBkndConfig, platform?: Platform)
async function action(options: {
port: number;
memory?: boolean;
config?: string;
dbUrl?: string;
dbToken?: string;
server: Platform;
}) {
replaceConsole();
const configFilePath = await getConfigPath(options.config);
let app: App | undefined = undefined;
if (options.dbUrl) {
console.info("Using connection from", c.cyan("--db-url"));
const connection = options.dbUrl
? { url: options.dbUrl, authToken: options.dbToken }
: undefined;
app = await makeApp({ connection, server: { platform: options.server } });
} else if (configFilePath) {
console.log("[INFO] Using config from:", configFilePath);
console.info("Using config from", c.cyan(configFilePath));
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
app = await makeConfigApp(config, options.server);
} else if (options.memory) {
console.info("Using", c.cyan("in-memory"), "connection");
app = await makeApp({ server: { platform: options.server } });
} else {
const credentials = getConnectionCredentialsFromEnv();
if (credentials) {
console.log("[INFO] Using connection from environment");
console.info("Using connection from env", c.cyan(credentials.url));
app = await makeConfigApp({ app: { connection: credentials } }, options.server);
}
}
if (!app) {
app = await makeApp({ server: { platform: options.server } });
const connection = { url: "file:data.db" } as Config;
console.info("Using connection", c.cyan(connection.url));
app = await makeApp({
connection,
server: { platform: options.server }
});
}
await startServer(options.server, app, { port: options.port });

View File

@@ -1,3 +1,6 @@
import { isDebug } from "core";
import c from "picocolors";
import type { Formatter } from "picocolors/types";
const _SPEEDUP = process.env.LOCAL;
const DEFAULT_WAIT = _SPEEDUP ? 0 : 250;
@@ -54,3 +57,31 @@ export async function* typewriter(
}
}
}
function ifString(args: any[], c: Formatter) {
return args.map((a) => (typeof a === "string" ? c(a) : a));
}
const originalConsole = {
log: console.log,
info: console.info,
debug: console.debug,
warn: console.warn,
error: console.error
};
export const $console = {
log: (...args: any[]) => originalConsole.info(c.gray("[LOG] "), ...ifString(args, c.dim)),
info: (...args: any[]) => originalConsole.info(c.cyan("[INFO] "), ...args),
debug: (...args: any[]) => isDebug() && originalConsole.info(c.yellow("[DEBUG]"), ...args),
warn: (...args: any[]) => originalConsole.info(c.yellow("[WARN] "), ...ifString(args, c.yellow)),
error: (...args: any[]) => originalConsole.info(c.red("[ERROR]"), ...ifString(args, c.red))
};
export function replaceConsole() {
console.log = $console.log;
console.info = $console.info;
console.debug = $console.debug;
console.warn = $console.warn;
console.error = $console.error;
}

View File

@@ -144,7 +144,7 @@ export class DataController extends Controller {
//console.log("request", c.req.raw);
const { entity, context } = c.req.param();
if (!this.entityExists(entity)) {
console.log("not found", entity, definedEntities);
console.warn("not found:", entity, definedEntities);
return c.notFound();
}
const _entity = this.em.entity(entity);
@@ -228,7 +228,7 @@ export class DataController extends Controller {
//console.log("request", c.req.raw);
const { entity } = c.req.param();
if (!this.entityExists(entity)) {
console.log("not found", entity, definedEntities);
console.warn("not found:", entity, definedEntities);
return c.notFound();
}
const options = c.req.valid("query") as RepoQuery;

View File

@@ -63,6 +63,8 @@ export class AppFlows extends Module<typeof flowsConfigSchema> {
});
});
hono.all("*", (c) => c.notFound());
this.ctx.server.route(this.config.basepath, hono);
// register flows

View File

@@ -7,6 +7,7 @@ import { html } from "hono/html";
import { Fragment } from "hono/jsx";
import { Controller } from "modules/Controller";
import * as SystemPermissions from "modules/permissions";
import type { AppTheme } from "modules/server/AppServer";
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
@@ -246,7 +247,7 @@ export class AdminController extends Controller {
}
}
const style = (theme: "light" | "dark" = "light") => {
const style = (theme: AppTheme) => {
const base = {
margin: 0,
padding: 0,
@@ -271,6 +272,6 @@ const style = (theme: "light" | "dark" = "light") => {
return {
...base,
...styles[theme]
...styles[theme === "light" ? "light" : "dark"]
};
};

View File

@@ -4,12 +4,15 @@ import { cors } from "hono/cors";
import { Module } from "modules/Module";
const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"];
const appThemes = ["dark", "light", "system"] as const;
export type AppTheme = (typeof appThemes)[number];
export const serverConfigSchema = Type.Object(
{
admin: Type.Object(
{
basepath: Type.Optional(Type.String({ default: "", pattern: "^(/.+)?$" })),
color_scheme: Type.Optional(StringEnum(["dark", "light"], { default: "light" })),
color_scheme: Type.Optional(StringEnum(["dark", "light", "system"])),
logo_return_path: Type.Optional(
Type.String({
default: "/",

View File

@@ -44,7 +44,7 @@ function AdminInternal() {
const { theme } = useTheme();
return (
<MantineProvider {...createMantineTheme(theme ?? "light")}>
<MantineProvider {...createMantineTheme(theme as any)}>
<Notifications />
<FlashMessage />
<BkndModalsProvider>

View File

@@ -1,5 +1,6 @@
import { Api, type ApiOptions, type TApiUser } from "Api";
import { isDebug } from "core";
import type { AppTheme } from "modules/server/AppServer";
import { createContext, useContext } from "react";
const ClientContext = createContext<{ baseUrl: string; api: Api }>({
@@ -61,7 +62,7 @@ export const useBaseUrl = () => {
type BkndWindowContext = {
user?: TApiUser;
logout_route: string;
color_scheme?: "light" | "dark";
color_scheme?: AppTheme;
};
export function useBkndWindowContext(): BkndWindowContext {
if (typeof window !== "undefined" && window.__BKND__) {

View File

@@ -1,18 +1,26 @@
import type { AppTheme } from "modules/server/AppServer";
import { useBkndWindowContext } from "ui/client/ClientProvider";
import { useBknd } from "ui/client/bknd";
export type Theme = "light" | "dark";
export function useTheme(fallback: Theme = "light"): { theme: Theme } {
export function useTheme(fallback: AppTheme = "system"): { theme: AppTheme } {
const b = useBknd();
const winCtx = useBkndWindowContext();
if (b) {
if (b?.adminOverride?.color_scheme) {
return { theme: b.adminOverride.color_scheme };
} else if (!b.fallback) {
return { theme: b.config.server.admin.color_scheme ?? fallback };
}
// 1. override
// 2. config
// 3. winCtx
// 4. fallback
// 5. default
const override = b?.adminOverride?.color_scheme;
const config = b?.config.server.admin.color_scheme;
const win = winCtx.color_scheme;
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const theme = override ?? config ?? win ?? fallback;
if (theme === "system") {
return { theme: prefersDark ? "dark" : "light" };
}
return { theme: winCtx.color_scheme ?? fallback };
return { theme };
}

View File

@@ -10,12 +10,6 @@ import MediaRoutes from "./media";
import { Root, RootEmpty } from "./root";
import SettingsRoutes from "./settings";
/*const DataRoutes = lazy(() => import("./data"));
const AuthRoutes = lazy(() => import("./auth"));
const MediaRoutes = lazy(() => import("./media"));
const FlowRoutes = lazy(() => import("./flows"));
const SettingsRoutes = lazy(() => import("./settings"));*/
// @ts-ignore
const TestRoutes = lazy(() => import("./test"));