refactor console verbosity and internal env handling

This commit is contained in:
dswbx
2025-03-04 11:18:14 +01:00
parent 36fba4588f
commit ab73b02138
13 changed files with 256 additions and 142 deletions

View File

@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test";
import { env, is_toggled } from "core/env";
describe("env", () => {
test("is_toggled", () => {
expect(is_toggled("true")).toBe(true);
expect(is_toggled("1")).toBe(true);
expect(is_toggled("false")).toBe(false);
expect(is_toggled("0")).toBe(false);
expect(is_toggled(true)).toBe(true);
expect(is_toggled(false)).toBe(false);
expect(is_toggled(undefined)).toBe(false);
expect(is_toggled(null)).toBe(false);
expect(is_toggled(1)).toBe(true);
expect(is_toggled(0)).toBe(false);
expect(is_toggled("anything else")).toBe(false);
});
test("env()", () => {
expect(env("cli_log_level", undefined, { source: {} })).toBeUndefined();
expect(env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "log" } })).toBe(
"log" as any,
);
expect(env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "LOG" } })).toBe(
"log" as any,
);
expect(
env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "asdf" } }),
).toBeUndefined();
expect(env("modules_debug", undefined, { source: {} })).toBeFalse();
expect(env("modules_debug", undefined, { source: { BKND_MODULES_DEBUG: "1" } })).toBeTrue();
expect(env("modules_debug", undefined, { source: { BKND_MODULES_DEBUG: "0" } })).toBeFalse();
});
});

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.9.0-rc.1-11", "version": "0.9.0-rc.2",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {

View File

@@ -5,7 +5,7 @@ import type { CliCommand } from "cli/types";
import { typewriter, wait } from "cli/utils/cli"; import { typewriter, wait } from "cli/utils/cli";
import { execAsync, getVersion } from "cli/utils/sys"; import { execAsync, getVersion } from "cli/utils/sys";
import { Option } from "commander"; import { Option } from "commander";
import { colorizeConsole } from "core"; import { env } from "core";
import color from "picocolors"; import color from "picocolors";
import { overridePackageJson, updateBkndPackages } from "./npm"; import { overridePackageJson, updateBkndPackages } from "./npm";
import { type Template, templates } from "./templates"; import { type Template, templates } from "./templates";
@@ -50,7 +50,6 @@ function errorOutro() {
async function action(options: { template?: string; dir?: string; integration?: string }) { async function action(options: { template?: string; dir?: string; integration?: string }) {
console.log(""); console.log("");
colorizeConsole(console);
const downloadOpts = { const downloadOpts = {
dir: options.dir || "./", dir: options.dir || "./",
@@ -59,7 +58,7 @@ async function action(options: { template?: string; dir?: string; integration?:
const version = await getVersion(); const version = await getVersion();
$p.intro( $p.intro(
`👋 Welcome to the ${color.bold(color.cyan("bknd"))} create wizard ${color.bold(`v${version}`)}`, `👋 Welcome to the ${color.bold(color.cyan("bknd"))} create cli ${color.bold(`v${version}`)}`,
); );
await $p.stream.message( await $p.stream.message(
@@ -178,10 +177,11 @@ async function action(options: { template?: string; dir?: string; integration?:
const ctx = { template, dir: downloadOpts.dir, name }; const ctx = { template, dir: downloadOpts.dir, name };
{ {
const ref = process.env.BKND_CLI_CREATE_REF ?? `v${version}`; const ref = env("cli_create_ref", `#v${version}`, {
if (process.env.BKND_CLI_CREATE_REF) { onValid: (given) => {
$p.log.warn(color.dim("[DEV] Using local ref: ") + color.yellow(ref)); $p.log.warn(color.dim("[DEV] Using local ref: ") + color.yellow(given));
} },
});
const prefix = const prefix =
template.ref === true template.ref === true

View File

@@ -8,7 +8,15 @@ export type TemplateSetupCtx = {
name: string; name: string;
}; };
export type Integration = "node" | "bun" | "cloudflare" | "nextjs" | "remix" | "astro" | "custom"; export type Integration =
| "node"
| "bun"
| "cloudflare"
| "nextjs"
| "remix"
| "astro"
| "aws"
| "custom";
type TemplateScripts = "install" | "dev" | "build" | "start"; type TemplateScripts = "install" | "dev" | "build" | "start";
export type Template = { export type Template = {
@@ -61,4 +69,12 @@ export const templates: Template[] = [
path: "gh:bknd-io/bknd/examples/astro", path: "gh:bknd-io/bknd/examples/astro",
ref: true, ref: true,
}, },
{
key: "aws",
title: "AWS Lambda Basic",
integration: "aws",
description: "A basic bknd AWS Lambda starter",
path: "gh:bknd-io/bknd/examples/aws-lambda",
ref: true,
},
]; ];

View File

@@ -1,5 +1,6 @@
import { datetimeStringLocal } from "core/utils"; import { datetimeStringLocal } from "core/utils";
import colors from "picocolors"; import colors from "picocolors";
import { env } from "core";
function hasColors() { function hasColors() {
try { try {
@@ -21,85 +22,74 @@ function hasColors() {
} }
} }
const originalConsoles = { const __consoles = {
error: console.error,
warn: console.warn,
info: console.info,
log: console.log,
debug: console.debug,
} as typeof console;
function __tty(type: any, args: any[]) {
const has = hasColors();
const styles = {
error: { error: {
prefix: colors.red, prefix: "ERR",
args: colors.red, color: colors.red,
args_color: colors.red,
original: console.error,
}, },
warn: { warn: {
prefix: colors.yellow, prefix: "WRN",
args: colors.yellow, color: colors.yellow,
args_color: colors.yellow,
original: console.warn,
}, },
info: { info: {
prefix: colors.cyan, prefix: "INF",
color: colors.cyan,
original: console.info,
}, },
log: { log: {
prefix: colors.dim, prefix: "LOG",
color: colors.dim,
args_color: colors.dim,
original: console.log,
}, },
debug: { debug: {
prefix: colors.yellow, prefix: "DBG",
args: colors.dim, color: colors.yellow,
args_color: colors.dim,
original: console.debug,
}, },
} as const; } as const;
const prefix = styles[type].prefix(`[${type.toUpperCase()}]`);
function __tty(_type: any, args: any[]) {
const has = hasColors();
const cons = __consoles[_type];
const prefix = cons.color(`[${cons.prefix}]`);
const _args = args.map((a) => const _args = args.map((a) =>
"args" in styles[type] && has && typeof a === "string" ? styles[type].args(a) : a, "args_color" in cons && has && typeof a === "string" ? cons.args_color(a) : a,
); );
return originalConsoles[type](prefix, colors.gray(datetimeStringLocal()), ..._args); return cons.original(prefix, colors.gray(datetimeStringLocal()), ..._args);
} }
export type TConsoleSeverity = keyof typeof originalConsoles; export type TConsoleSeverity = keyof typeof __consoles;
const severities = Object.keys(originalConsoles) as TConsoleSeverity[]; const level = env("cli_log_level", "log");
let enabled = [...severities];
export function disableConsole(severities: TConsoleSeverity[] = enabled) {
enabled = enabled.filter((s) => !severities.includes(s));
}
export function enableConsole() {
enabled = [...severities];
}
const keys = Object.keys(__consoles);
export const $console = new Proxy( export const $console = new Proxy(
{}, {},
{ {
get: (_, prop) => { get: (_, prop) => {
if (prop in originalConsoles && enabled.includes(prop as TConsoleSeverity)) { if (prop === "original") {
return console;
}
const current = keys.indexOf(level as string);
const requested = keys.indexOf(prop as string);
if (prop in __consoles && requested <= current) {
return (...args: any[]) => __tty(prop, args); return (...args: any[]) => __tty(prop, args);
} }
return () => null; return () => null;
}, },
}, },
) as typeof console; ) as typeof console & {
original: typeof console;
export async function withDisabledConsole<R>( };
fn: () => Promise<R>,
sev?: TConsoleSeverity[],
): Promise<R> {
disableConsole(sev);
try {
const result = await fn();
enableConsole();
return result;
} catch (e) {
enableConsole();
throw e;
}
}
export function colorizeConsole(con: typeof console) { export function colorizeConsole(con: typeof console) {
for (const [key] of Object.entries(originalConsoles)) { for (const [key] of Object.entries(__consoles)) {
con[key] = $console[key]; con[key] = $console[key];
} }
} }

View File

@@ -1,27 +1,72 @@
type TURSO_DB = { export type Env = {};
url: string;
authToken: string;
};
export type Env = { export const is_toggled = (given: unknown): boolean => {
__STATIC_CONTENT: Fetcher; return typeof given === "string" ? [1, "1", "true"].includes(given) : Boolean(given);
ENVIRONMENT: string;
CACHE: KVNamespace;
// db
DB_DATA: TURSO_DB;
DB_SCHEMA: TURSO_DB;
// storage
STORAGE: { access_key: string; secret_access_key: string; url: string };
BUCKET: R2Bucket;
}; };
export function isDebug(): boolean { export function isDebug(): boolean {
try { try {
// @ts-expect-error - this is a global variable in dev // @ts-expect-error - this is a global variable in dev
return __isDev === "1" || __isDev === 1; return is_toggled(__isDev);
} catch (e) { } catch (e) {
return false; return false;
} }
} }
const envs = {
// used in $console to determine the log level
cli_log_level: {
key: "BKND_CLI_LOG_LEVEL",
validate: (v: unknown) => {
if (
typeof v === "string" &&
["log", "info", "warn", "error", "debug"].includes(v.toLowerCase())
) {
return v.toLowerCase() as keyof typeof console;
}
return undefined;
},
},
// cli create, determine ref to download template
cli_create_ref: {
key: "BKND_CLI_CREATE_REF",
validate: (v: unknown) => {
return typeof v === "string" ? v : undefined;
},
},
// module manager debug: {
modules_debug: {
key: "BKND_MODULES_DEBUG",
validate: is_toggled,
},
} as const;
export const env = <
Key extends keyof typeof envs,
Fallback = any,
R = ReturnType<(typeof envs)[Key]["validate"]>,
>(
key: Key,
fallback?: Fallback,
opts?: {
source?: any;
onFallback?: (given: unknown) => void;
onValid?: (valid: R) => void;
},
): R extends undefined ? Fallback : R => {
try {
const source = opts?.source ?? process.env;
const c = envs[key];
const g = source[c.key];
const v = c.validate(g) as any;
if (typeof v !== "undefined") {
opts?.onValid?.(v);
return v;
}
opts?.onFallback?.(g);
} catch (e) {
opts?.onFallback?.(undefined);
}
return fallback as any;
};

View File

@@ -2,7 +2,7 @@ import type { Hono, MiddlewareHandler } from "hono";
export { tbValidator } from "./server/lib/tbValidator"; export { tbValidator } from "./server/lib/tbValidator";
export { Exception, BkndError } from "./errors"; export { Exception, BkndError } from "./errors";
export { isDebug } from "./env"; export { isDebug, env } from "./env";
export { type PrimaryFieldType, config, type DB } from "./config"; export { type PrimaryFieldType, config, type DB } from "./config";
export { AwsClient } from "./clients/aws/AwsClient"; export { AwsClient } from "./clients/aws/AwsClient";
export { export {

View File

@@ -14,7 +14,7 @@ import type { EntityRelation } from "../relations";
import { RelationAccessor } from "../relations/RelationAccessor"; import { RelationAccessor } from "../relations/RelationAccessor";
import { SchemaManager } from "../schema/SchemaManager"; import { SchemaManager } from "../schema/SchemaManager";
import { Entity } from "./Entity"; import { Entity } from "./Entity";
import { type EntityData, Mutator, Repository } from "./index"; import { type EntityData, Mutator, Repository, type RepositoryOptions } from "./index";
type EntitySchema< type EntitySchema<
TBD extends object = DefaultDB, TBD extends object = DefaultDB,
@@ -211,12 +211,15 @@ export class EntityManager<TBD extends object = DefaultDB> {
return this.repo(entity); return this.repo(entity);
} }
repo<E extends Entity | keyof TBD | string>(entity: E): Repository<TBD, EntitySchema<TBD, E>> { repo<E extends Entity | keyof TBD | string>(
return new Repository(this, this.entity(entity), this.emgr); entity: E,
opts: Omit<RepositoryOptions, "emgr"> = {},
): Repository<TBD, EntitySchema<TBD, E>> {
return new Repository(this, this.entity(entity), { ...opts, emgr: this.emgr });
} }
mutator<E extends Entity | keyof TBD | string>(entity: E): Mutator<TBD, EntitySchema<TBD, E>> { mutator<E extends Entity | keyof TBD | string>(entity: E): Mutator<TBD, EntitySchema<TBD, E>> {
return new Mutator(this, this.entity(entity), this.emgr); return new Mutator(this, this.entity(entity), { emgr: this.emgr });
} }
addIndex(index: EntityIndex, force = false) { addIndex(index: EntityIndex, force = false) {

View File

@@ -32,8 +32,6 @@ export class Mutator<
Input = Omit<Output, "id">, Input = Omit<Output, "id">,
> implements EmitsEvents > implements EmitsEvents
{ {
em: EntityManager<TBD>;
entity: Entity;
static readonly Events = MutatorEvents; static readonly Events = MutatorEvents;
emgr: EventManager<typeof MutatorEvents>; emgr: EventManager<typeof MutatorEvents>;
@@ -43,10 +41,12 @@ export class Mutator<
this.__unstable_disable_system_entity_creation = value; this.__unstable_disable_system_entity_creation = value;
} }
constructor(em: EntityManager<TBD>, entity: Entity, emgr?: EventManager<any>) { constructor(
this.em = em; public em: EntityManager<TBD>,
this.entity = entity; public entity: Entity,
this.emgr = emgr ?? new EventManager(MutatorEvents); protected options?: { emgr?: EventManager<any> },
) {
this.emgr = options?.emgr ?? new EventManager(MutatorEvents);
} }
private get conn() { private get conn() {

View File

@@ -4,7 +4,7 @@ import { type EmitsEvents, EventManager } from "core/events";
import { type SelectQueryBuilder, sql } from "kysely"; import { type SelectQueryBuilder, sql } from "kysely";
import { cloneDeep } from "lodash-es"; import { cloneDeep } from "lodash-es";
import { InvalidSearchParamsException } from "../../errors"; import { InvalidSearchParamsException } from "../../errors";
import { MutatorEvents, RepositoryEvents, RepositoryFindManyBefore } from "../../events"; import { MutatorEvents, RepositoryEvents } from "../../events";
import { type RepoQuery, defaultQuerySchema } from "../../server/data-query-impl"; import { type RepoQuery, defaultQuerySchema } from "../../server/data-query-impl";
import { import {
type Entity, type Entity,
@@ -44,22 +44,27 @@ export type RepositoryExistsResponse = RepositoryRawResponse & {
exists: boolean; exists: boolean;
}; };
export type RepositoryOptions = {
silent?: boolean;
emgr?: EventManager<any>;
};
export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = any> export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = any>
implements EmitsEvents implements EmitsEvents
{ {
em: EntityManager<TBD>;
entity: Entity;
static readonly Events = RepositoryEvents; static readonly Events = RepositoryEvents;
emgr: EventManager<typeof Repository.Events>; emgr: EventManager<typeof Repository.Events>;
constructor(em: EntityManager<TBD>, entity: Entity, emgr?: EventManager<any>) { constructor(
this.em = em; public em: EntityManager<TBD>,
this.entity = entity; public entity: Entity,
this.emgr = emgr ?? new EventManager(MutatorEvents); protected options?: RepositoryOptions,
) {
this.emgr = options?.emgr ?? new EventManager(MutatorEvents);
} }
private cloneFor(entity: Entity) { private cloneFor(entity: Entity) {
return new Repository(this.em, this.em.entity(entity), this.emgr); return new Repository(this.em, this.em.entity(entity), { emgr: this.emgr });
} }
private get conn() { private get conn() {
@@ -68,7 +73,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
private checkIndex(entity: string, field: string, clause: string) { private checkIndex(entity: string, field: string, clause: string) {
const indexed = this.em.getIndexedFields(entity).map((f) => f.name); const indexed = this.em.getIndexedFields(entity).map((f) => f.name);
if (!indexed.includes(field)) { if (!indexed.includes(field) && this.options?.silent !== true) {
$console.warn(`Field "${entity}.${field}" used in "${clause}" is not indexed`); $console.warn(`Field "${entity}.${field}" used in "${clause}" is not indexed`);
} }
} }
@@ -174,7 +179,9 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> { protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
const entity = this.entity; const entity = this.entity;
const compiled = qb.compile(); const compiled = qb.compile();
//$console.debug(`Repository: query\n${compiled.sql}\n`, compiled.parameters); if (this.options?.silent !== true) {
$console.debug(`Repository: query\n${compiled.sql}\n`, compiled.parameters);
}
const start = performance.now(); const start = performance.now();
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as); const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
@@ -186,6 +193,20 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
.clearGroupBy() .clearGroupBy()
.clearOrderBy(); .clearOrderBy();
const totalQuery = this.conn.selectFrom(entity.name).select(selector("total")); const totalQuery = this.conn.selectFrom(entity.name).select(selector("total"));
const payload = {
entity,
sql: compiled.sql,
parameters: [...compiled.parameters],
result: [],
data: [],
meta: {
total: 0,
count: 0,
items: 0,
time: 0,
query: { sql: compiled.sql, parameters: compiled.parameters },
},
};
try { try {
const [_count, _total, result] = await this.em.connection.batchQuery([ const [_count, _total, result] = await this.em.connection.batchQuery([
@@ -199,25 +220,27 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
const data = this.em.hydrate(entity.name, result); const data = this.em.hydrate(entity.name, result);
return { return {
entity, ...payload,
sql: compiled.sql,
parameters: [...compiled.parameters],
result, result,
data, data,
meta: { meta: {
...payload.meta,
total: _total[0]?.total ?? 0, total: _total[0]?.total ?? 0,
count: _count[0]?.count ?? 0, // @todo: better graceful method count: _count[0]?.count ?? 0, // @todo: better graceful method
items: result.length, items: result.length,
time, time,
query: { sql: compiled.sql, parameters: compiled.parameters },
}, },
}; };
} catch (e) { } catch (e) {
if (this.options?.silent !== true) {
if (e instanceof Error) { if (e instanceof Error) {
$console.error("[ERROR] Repository.performQuery", e.message); $console.error("[ERROR] Repository.performQuery", e.message);
} }
throw e; throw e;
} else {
return payload;
}
} }
} }

View File

@@ -1,5 +1,5 @@
import { Guard } from "auth"; import { Guard } from "auth";
import { $console, BkndError, DebugLogger, withDisabledConsole } from "core"; import { $console, BkndError, DebugLogger, env } from "core";
import { EventManager } from "core/events"; import { EventManager } from "core/events";
import { clone, diff } from "core/object/diff"; import { clone, diff } from "core/object/diff";
import { import {
@@ -91,7 +91,7 @@ export type ModuleManagerOptions = {
trustFetched?: boolean; trustFetched?: boolean;
// runs when initial config provided on a fresh database // runs when initial config provided on a fresh database
seed?: (ctx: ModuleBuildContext) => Promise<void>; seed?: (ctx: ModuleBuildContext) => Promise<void>;
// wether /** @deprecated */
verbosity?: Verbosity; verbosity?: Verbosity;
}; };
@@ -127,6 +127,8 @@ interface T_INTERNAL_EM {
__bknd: ConfigTable2; __bknd: ConfigTable2;
} }
const debug_modules = env("modules_debug");
// @todo: cleanup old diffs on upgrade // @todo: cleanup old diffs on upgrade
// @todo: cleanup multiple backups on upgrade // @todo: cleanup multiple backups on upgrade
export class ModuleManager { export class ModuleManager {
@@ -152,7 +154,7 @@ export class ModuleManager {
this.__em = new EntityManager([__bknd], this.connection); this.__em = new EntityManager([__bknd], this.connection);
this.modules = {} as Modules; this.modules = {} as Modules;
this.emgr = new EventManager(); this.emgr = new EventManager();
this.logger = new DebugLogger(this.verbosity === Verbosity.log); this.logger = new DebugLogger(debug_modules);
let initial = {} as Partial<ModuleConfigs>; let initial = {} as Partial<ModuleConfigs>;
if (options?.initial) { if (options?.initial) {
@@ -215,7 +217,9 @@ export class ModuleManager {
} }
private repo() { private repo() {
return this.__em.repo(__bknd); return this.__em.repo(__bknd, {
silent: !debug_modules,
});
} }
private mutator() { private mutator() {
@@ -226,6 +230,7 @@ export class ModuleManager {
return this.connection.kysely as Kysely<{ table: ConfigTable }>; return this.connection.kysely as Kysely<{ table: ConfigTable }>;
} }
// @todo: add indices for: version, type
async syncConfigTable() { async syncConfigTable() {
this.logger.context("sync").log("start"); this.logger.context("sync").log("start");
const result = await this.__em.schema().sync({ force: true }); const result = await this.__em.schema().sync({ force: true });
@@ -271,8 +276,6 @@ export class ModuleManager {
const startTime = performance.now(); const startTime = performance.now();
// disabling console log, because the table might not exist yet // disabling console log, because the table might not exist yet
const result = await withDisabledConsole(
async () => {
const { data: result } = await this.repo().findOne( const { data: result } = await this.repo().findOne(
{ type: "config" }, { type: "config" },
{ {
@@ -281,21 +284,18 @@ export class ModuleManager {
); );
if (!result) { if (!result) {
this.logger.log("error fetching").clear();
throw BkndError.with("no config"); throw BkndError.with("no config");
} }
return result as unknown as ConfigTable;
},
this.verbosity > Verbosity.silent ? [] : ["error"],
);
this.logger this.logger
.log("took", performance.now() - startTime, "ms", { .log("took", performance.now() - startTime, "ms", {
version: result.version, version: result.version,
id: result.id, id: result.id,
}) })
.clear(); .clear();
return result;
return result as unknown as ConfigTable;
} }
async save() { async save() {
@@ -412,7 +412,6 @@ export class ModuleManager {
); );
} }
} catch (e: any) { } catch (e: any) {
this.logger.clear(); // fetch couldn't clear
throw new Error(`Version is ${this.version()}, fetch failed: ${e.message}`); throw new Error(`Version is ${this.version()}, fetch failed: ${e.message}`);
} }

View File

@@ -49,11 +49,14 @@ export default {
// log routes // log routes
if (firstStart) { if (firstStart) {
console.log("[DB]", credentials);
firstStart = false; firstStart = false;
/*console.log("\n[APP ROUTES]"); console.log("[DB]", credentials);
if (import.meta.env.VITE_SHOW_ROUTES === "1") {
console.log("\n[APP ROUTES]");
showRoutes(app.server); showRoutes(app.server);
console.log("-------\n");*/ console.log("-------\n");
}
} }
} }

View File

@@ -35,7 +35,7 @@ export const handler = serveLambda({
} }
}); });
``` ```
Although the runtime would support database as a file, we don't recommend it. You'd need to also bundle the native dependencies which increases the deployment size and cold start time. Although the runtime would support database as a file, we don't recommend it. You'd need to also bundle the native dependencies which increases the deployment size and cold start time. Instead, we recommend you to use [LibSQL on Turso](/usage/database#sqlite-using-libsql-on-turso).
## Serve the Admin UI ## Serve the Admin UI
Lambda functions should be as small as possible. Therefore, the static files for the admin panel should not be served from node_modules like with the Node adapter. Lambda functions should be as small as possible. Therefore, the static files for the admin panel should not be served from node_modules like with the Node adapter.