From ab73b021382a1b3da157d41423bed11445e00b07 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 4 Mar 2025 11:18:14 +0100 Subject: [PATCH] refactor console verbosity and internal env handling --- app/__test__/core/env.spec.ts | 35 ++++++ app/package.json | 2 +- app/src/cli/commands/create/create.ts | 14 +-- .../cli/commands/create/templates/index.ts | 18 ++- app/src/core/console.ts | 112 ++++++++---------- app/src/core/env.ts | 79 +++++++++--- app/src/core/index.ts | 2 +- app/src/data/entities/EntityManager.ts | 11 +- app/src/data/entities/Mutator.ts | 12 +- app/src/data/entities/query/Repository.ts | 59 ++++++--- app/src/modules/ModuleManager.ts | 41 ++++--- app/vite.dev.ts | 11 +- docs/integration/aws.mdx | 2 +- 13 files changed, 256 insertions(+), 142 deletions(-) create mode 100644 app/__test__/core/env.spec.ts diff --git a/app/__test__/core/env.spec.ts b/app/__test__/core/env.spec.ts new file mode 100644 index 0000000..d4c5ba3 --- /dev/null +++ b/app/__test__/core/env.spec.ts @@ -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(); + }); +}); diff --git a/app/package.json b/app/package.json index 8b636fe..074b291 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "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.", "homepage": "https://bknd.io", "repository": { diff --git a/app/src/cli/commands/create/create.ts b/app/src/cli/commands/create/create.ts index c5fe02a..f1f9a5e 100644 --- a/app/src/cli/commands/create/create.ts +++ b/app/src/cli/commands/create/create.ts @@ -5,7 +5,7 @@ import type { CliCommand } from "cli/types"; import { typewriter, wait } from "cli/utils/cli"; import { execAsync, getVersion } from "cli/utils/sys"; import { Option } from "commander"; -import { colorizeConsole } from "core"; +import { env } from "core"; import color from "picocolors"; import { overridePackageJson, updateBkndPackages } from "./npm"; import { type Template, templates } from "./templates"; @@ -50,7 +50,6 @@ function errorOutro() { async function action(options: { template?: string; dir?: string; integration?: string }) { console.log(""); - colorizeConsole(console); const downloadOpts = { dir: options.dir || "./", @@ -59,7 +58,7 @@ async function action(options: { template?: string; dir?: string; integration?: const version = await getVersion(); $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( @@ -178,10 +177,11 @@ async function action(options: { template?: string; dir?: string; integration?: const ctx = { template, dir: downloadOpts.dir, name }; { - const ref = process.env.BKND_CLI_CREATE_REF ?? `v${version}`; - if (process.env.BKND_CLI_CREATE_REF) { - $p.log.warn(color.dim("[DEV] Using local ref: ") + color.yellow(ref)); - } + const ref = env("cli_create_ref", `#v${version}`, { + onValid: (given) => { + $p.log.warn(color.dim("[DEV] Using local ref: ") + color.yellow(given)); + }, + }); const prefix = template.ref === true diff --git a/app/src/cli/commands/create/templates/index.ts b/app/src/cli/commands/create/templates/index.ts index b128e44..f3cf9b5 100644 --- a/app/src/cli/commands/create/templates/index.ts +++ b/app/src/cli/commands/create/templates/index.ts @@ -8,7 +8,15 @@ export type TemplateSetupCtx = { 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"; export type Template = { @@ -61,4 +69,12 @@ export const templates: Template[] = [ path: "gh:bknd-io/bknd/examples/astro", 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, + }, ]; diff --git a/app/src/core/console.ts b/app/src/core/console.ts index 17424fb..2d8e11b 100644 --- a/app/src/core/console.ts +++ b/app/src/core/console.ts @@ -1,5 +1,6 @@ import { datetimeStringLocal } from "core/utils"; import colors from "picocolors"; +import { env } from "core"; function hasColors() { try { @@ -21,85 +22,74 @@ function hasColors() { } } -const originalConsoles = { - error: console.error, - warn: console.warn, - info: console.info, - log: console.log, - debug: console.debug, -} as typeof console; +const __consoles = { + error: { + prefix: "ERR", + color: colors.red, + args_color: colors.red, + original: console.error, + }, + warn: { + prefix: "WRN", + color: colors.yellow, + args_color: colors.yellow, + original: console.warn, + }, + info: { + prefix: "INF", + color: colors.cyan, + original: console.info, + }, + log: { + prefix: "LOG", + color: colors.dim, + args_color: colors.dim, + original: console.log, + }, + debug: { + prefix: "DBG", + color: colors.yellow, + args_color: colors.dim, + original: console.debug, + }, +} as const; -function __tty(type: any, args: any[]) { +function __tty(_type: any, args: any[]) { const has = hasColors(); - const styles = { - error: { - prefix: colors.red, - args: colors.red, - }, - warn: { - prefix: colors.yellow, - args: colors.yellow, - }, - info: { - prefix: colors.cyan, - }, - log: { - prefix: colors.dim, - }, - debug: { - prefix: colors.yellow, - args: colors.dim, - }, - } as const; - const prefix = styles[type].prefix(`[${type.toUpperCase()}]`); + const cons = __consoles[_type]; + const prefix = cons.color(`[${cons.prefix}]`); 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; -const severities = Object.keys(originalConsoles) as TConsoleSeverity[]; - -let enabled = [...severities]; - -export function disableConsole(severities: TConsoleSeverity[] = enabled) { - enabled = enabled.filter((s) => !severities.includes(s)); -} - -export function enableConsole() { - enabled = [...severities]; -} +export type TConsoleSeverity = keyof typeof __consoles; +const level = env("cli_log_level", "log"); +const keys = Object.keys(__consoles); export const $console = new Proxy( {}, { 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 () => null; }, }, -) as typeof console; - -export async function withDisabledConsole( - fn: () => Promise, - sev?: TConsoleSeverity[], -): Promise { - disableConsole(sev); - try { - const result = await fn(); - enableConsole(); - return result; - } catch (e) { - enableConsole(); - throw e; - } -} +) as typeof console & { + original: 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]; } } diff --git a/app/src/core/env.ts b/app/src/core/env.ts index 386ec9d..0dd60ec 100644 --- a/app/src/core/env.ts +++ b/app/src/core/env.ts @@ -1,27 +1,72 @@ -type TURSO_DB = { - url: string; - authToken: string; -}; +export type Env = {}; -export type Env = { - __STATIC_CONTENT: Fetcher; - 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 const is_toggled = (given: unknown): boolean => { + return typeof given === "string" ? [1, "1", "true"].includes(given) : Boolean(given); }; export function isDebug(): boolean { try { // @ts-expect-error - this is a global variable in dev - return __isDev === "1" || __isDev === 1; + return is_toggled(__isDev); } catch (e) { 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; +}; diff --git a/app/src/core/index.ts b/app/src/core/index.ts index c9d0590..5c63a4b 100644 --- a/app/src/core/index.ts +++ b/app/src/core/index.ts @@ -2,7 +2,7 @@ import type { Hono, MiddlewareHandler } from "hono"; export { tbValidator } from "./server/lib/tbValidator"; export { Exception, BkndError } from "./errors"; -export { isDebug } from "./env"; +export { isDebug, env } from "./env"; export { type PrimaryFieldType, config, type DB } from "./config"; export { AwsClient } from "./clients/aws/AwsClient"; export { diff --git a/app/src/data/entities/EntityManager.ts b/app/src/data/entities/EntityManager.ts index c7f9c5b..8d36cf3 100644 --- a/app/src/data/entities/EntityManager.ts +++ b/app/src/data/entities/EntityManager.ts @@ -14,7 +14,7 @@ import type { EntityRelation } from "../relations"; import { RelationAccessor } from "../relations/RelationAccessor"; import { SchemaManager } from "../schema/SchemaManager"; import { Entity } from "./Entity"; -import { type EntityData, Mutator, Repository } from "./index"; +import { type EntityData, Mutator, Repository, type RepositoryOptions } from "./index"; type EntitySchema< TBD extends object = DefaultDB, @@ -211,12 +211,15 @@ export class EntityManager { return this.repo(entity); } - repo(entity: E): Repository> { - return new Repository(this, this.entity(entity), this.emgr); + repo( + entity: E, + opts: Omit = {}, + ): Repository> { + return new Repository(this, this.entity(entity), { ...opts, emgr: this.emgr }); } mutator(entity: E): Mutator> { - return new Mutator(this, this.entity(entity), this.emgr); + return new Mutator(this, this.entity(entity), { emgr: this.emgr }); } addIndex(index: EntityIndex, force = false) { diff --git a/app/src/data/entities/Mutator.ts b/app/src/data/entities/Mutator.ts index 8877ff3..d6f49db 100644 --- a/app/src/data/entities/Mutator.ts +++ b/app/src/data/entities/Mutator.ts @@ -32,8 +32,6 @@ export class Mutator< Input = Omit, > implements EmitsEvents { - em: EntityManager; - entity: Entity; static readonly Events = MutatorEvents; emgr: EventManager; @@ -43,10 +41,12 @@ export class Mutator< this.__unstable_disable_system_entity_creation = value; } - constructor(em: EntityManager, entity: Entity, emgr?: EventManager) { - this.em = em; - this.entity = entity; - this.emgr = emgr ?? new EventManager(MutatorEvents); + constructor( + public em: EntityManager, + public entity: Entity, + protected options?: { emgr?: EventManager }, + ) { + this.emgr = options?.emgr ?? new EventManager(MutatorEvents); } private get conn() { diff --git a/app/src/data/entities/query/Repository.ts b/app/src/data/entities/query/Repository.ts index 971a008..4734ddf 100644 --- a/app/src/data/entities/query/Repository.ts +++ b/app/src/data/entities/query/Repository.ts @@ -4,7 +4,7 @@ import { type EmitsEvents, EventManager } from "core/events"; import { type SelectQueryBuilder, sql } from "kysely"; import { cloneDeep } from "lodash-es"; 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 Entity, @@ -44,22 +44,27 @@ export type RepositoryExistsResponse = RepositoryRawResponse & { exists: boolean; }; +export type RepositoryOptions = { + silent?: boolean; + emgr?: EventManager; +}; + export class Repository implements EmitsEvents { - em: EntityManager; - entity: Entity; static readonly Events = RepositoryEvents; emgr: EventManager; - constructor(em: EntityManager, entity: Entity, emgr?: EventManager) { - this.em = em; - this.entity = entity; - this.emgr = emgr ?? new EventManager(MutatorEvents); + constructor( + public em: EntityManager, + public entity: Entity, + protected options?: RepositoryOptions, + ) { + this.emgr = options?.emgr ?? new EventManager(MutatorEvents); } 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() { @@ -68,7 +73,7 @@ export class Repository 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`); } } @@ -174,7 +179,9 @@ export class Repository { const entity = this.entity; 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 selector = (as = "count") => this.conn.fn.countAll().as(as); @@ -186,6 +193,20 @@ export class Repository Promise; - // wether + /** @deprecated */ verbosity?: Verbosity; }; @@ -127,6 +127,8 @@ interface T_INTERNAL_EM { __bknd: ConfigTable2; } +const debug_modules = env("modules_debug"); + // @todo: cleanup old diffs on upgrade // @todo: cleanup multiple backups on upgrade export class ModuleManager { @@ -152,7 +154,7 @@ export class ModuleManager { this.__em = new EntityManager([__bknd], this.connection); this.modules = {} as Modules; this.emgr = new EventManager(); - this.logger = new DebugLogger(this.verbosity === Verbosity.log); + this.logger = new DebugLogger(debug_modules); let initial = {} as Partial; if (options?.initial) { @@ -215,7 +217,9 @@ export class ModuleManager { } private repo() { - return this.__em.repo(__bknd); + return this.__em.repo(__bknd, { + silent: !debug_modules, + }); } private mutator() { @@ -226,6 +230,7 @@ export class ModuleManager { return this.connection.kysely as Kysely<{ table: ConfigTable }>; } + // @todo: add indices for: version, type async syncConfigTable() { this.logger.context("sync").log("start"); const result = await this.__em.schema().sync({ force: true }); @@ -271,31 +276,26 @@ export class ModuleManager { const startTime = performance.now(); // disabling console log, because the table might not exist yet - const result = await withDisabledConsole( - async () => { - const { data: result } = await this.repo().findOne( - { type: "config" }, - { - sort: { by: "version", dir: "desc" }, - }, - ); - - if (!result) { - throw BkndError.with("no config"); - } - - return result as unknown as ConfigTable; + const { data: result } = await this.repo().findOne( + { type: "config" }, + { + sort: { by: "version", dir: "desc" }, }, - this.verbosity > Verbosity.silent ? [] : ["error"], ); + if (!result) { + this.logger.log("error fetching").clear(); + throw BkndError.with("no config"); + } + this.logger .log("took", performance.now() - startTime, "ms", { version: result.version, id: result.id, }) .clear(); - return result; + + return result as unknown as ConfigTable; } async save() { @@ -412,7 +412,6 @@ export class ModuleManager { ); } } catch (e: any) { - this.logger.clear(); // fetch couldn't clear throw new Error(`Version is ${this.version()}, fetch failed: ${e.message}`); } diff --git a/app/vite.dev.ts b/app/vite.dev.ts index b30ec87..69a2d9a 100644 --- a/app/vite.dev.ts +++ b/app/vite.dev.ts @@ -49,11 +49,14 @@ export default { // log routes if (firstStart) { - console.log("[DB]", credentials); firstStart = false; - /*console.log("\n[APP ROUTES]"); - showRoutes(app.server); - console.log("-------\n");*/ + console.log("[DB]", credentials); + + if (import.meta.env.VITE_SHOW_ROUTES === "1") { + console.log("\n[APP ROUTES]"); + showRoutes(app.server); + console.log("-------\n"); + } } } diff --git a/docs/integration/aws.mdx b/docs/integration/aws.mdx index b96c0db..1318571 100644 --- a/docs/integration/aws.mdx +++ b/docs/integration/aws.mdx @@ -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 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.