From db101889456f22a84dc9dedca293a7cde9669591 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 18 Jan 2025 12:39:34 +0100 Subject: [PATCH] strengthened schema ensuring for system entities --- app/__test__/modules/AppAuth.spec.ts | 38 ++++++++++++++++++-- app/__test__/modules/AppMedia.spec.ts | 4 +-- app/__test__/modules/Module.spec.ts | 3 +- app/package.json | 2 +- app/src/App.ts | 2 ++ app/src/auth/AppAuth.ts | 32 ++++++++++------- app/src/data/entities/EntityManager.ts | 4 +-- app/src/data/fields/Field.ts | 3 ++ app/src/modules/Module.ts | 41 +++++++++++++++++----- app/src/modules/ModuleManager.ts | 17 ++++++--- app/src/modules/server/SystemController.ts | 5 +++ app/src/ui/main.css | 3 -- 12 files changed, 118 insertions(+), 36 deletions(-) diff --git a/app/__test__/modules/AppAuth.spec.ts b/app/__test__/modules/AppAuth.spec.ts index 225c9d6..f0ecc86 100644 --- a/app/__test__/modules/AppAuth.spec.ts +++ b/app/__test__/modules/AppAuth.spec.ts @@ -1,7 +1,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test"; import { createApp } from "../../src"; import { AuthController } from "../../src/auth/api/AuthController"; -import { em, entity, text } from "../../src/data"; +import { em, entity, make, text } from "../../src/data"; import { AppAuth, type ModuleBuildContext } from "../../src/modules"; import { disableConsoleLog, enableConsoleLog } from "../helper"; import { makeCtx, moduleTestSuite } from "./module-test-suite"; @@ -125,6 +125,40 @@ describe("AppAuth", () => { const fields = e.fields.map((f) => f.name); expect(e.type).toBe("system"); expect(fields).toContain("additional"); - expect(fields).toEqual(["id", "email", "strategy", "strategy_value", "role", "additional"]); + expect(fields).toEqual(["id", "additional", "email", "strategy", "strategy_value", "role"]); + }); + + test("ensure user field configs is always correct", async () => { + const app = createApp({ + initialConfig: { + auth: { + enabled: true + }, + data: em({ + users: entity("users", { + strategy: text({ + fillable: true, + hidden: false + }), + strategy_value: text({ + fillable: true, + hidden: false + }) + }) + }).toJSON() + } + }); + await app.build(); + + const users = app.em.entity("users"); + const props = ["hidden", "fillable", "required"]; + + for (const [name, _authFieldProto] of Object.entries(AppAuth.usersFields)) { + const authField = make(name, _authFieldProto as any); + const field = users.field(name)!; + for (const prop of props) { + expect(field.config[prop]).toBe(authField.config[prop]); + } + } }); }); diff --git a/app/__test__/modules/AppMedia.spec.ts b/app/__test__/modules/AppMedia.spec.ts index 19fa73b..b5ce17f 100644 --- a/app/__test__/modules/AppMedia.spec.ts +++ b/app/__test__/modules/AppMedia.spec.ts @@ -39,6 +39,7 @@ describe("AppMedia", () => { expect(fields).toContain("additional"); expect(fields).toEqual([ "id", + "additional", "path", "folder", "mime_type", @@ -48,8 +49,7 @@ describe("AppMedia", () => { "modified_at", "reference", "entity_id", - "metadata", - "additional" + "metadata" ]); }); }); diff --git a/app/__test__/modules/Module.spec.ts b/app/__test__/modules/Module.spec.ts index 572c5a1..5c20ca5 100644 --- a/app/__test__/modules/Module.spec.ts +++ b/app/__test__/modules/Module.spec.ts @@ -157,8 +157,7 @@ describe("Module", async () => { entities: [ { name: "u", - // ensured properties must come first - fields: ["id", "important", "name"], + fields: ["id", "name", "important"], // ensured type must be present type: "system" }, diff --git a/app/package.json b/app/package.json index 4de3aa0..94b6912 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.0-rc.11", + "version": "0.6.0-rc.12", "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/App.ts b/app/src/App.ts index b98fc67..322554d 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -118,6 +118,8 @@ export class App { this.trigger_first_boot = false; await this.emgr.emit(new AppFirstBoot({ app: this })); } + + console.log("[APP] built"); } mutateConfig(module: Module) { diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index 20386a5..ca5b919 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -10,7 +10,7 @@ import type { PasswordStrategy } from "auth/authenticate/strategies"; import { type DB, Exception, type PrimaryFieldType } from "core"; import { type Static, secureRandomString, transformObject } from "core/utils"; import type { Entity, EntityManager } from "data"; -import { type FieldSchema, em, entity, enumm, make, text } from "data/prototype"; +import { type FieldSchema, em, entity, enumm, text } from "data/prototype"; import { pick } from "lodash-es"; import { Module } from "modules/Module"; import { AuthController } from "./api/AuthController"; @@ -224,17 +224,22 @@ export class AppAuth extends Module { } private toggleStrategyValueVisibility(visible: boolean) { - const field = this.getUsersEntity().field("strategy_value")!; + const toggle = (name: string, visible: boolean) => { + const field = this.getUsersEntity().field(name)!; - if (visible) { - field.config.hidden = false; - field.config.fillable = true; - } else { - // reset to normal - const template = AppAuth.usersFields.strategy_value.config; - field.config.hidden = template.hidden; - field.config.fillable = template.fillable; - } + if (visible) { + field.config.hidden = false; + field.config.fillable = true; + } else { + // reset to normal + const template = AppAuth.usersFields.strategy_value.config; + field.config.hidden = template.hidden; + field.config.fillable = template.fillable; + } + }; + + toggle("strategy_value", visible); + toggle("strategy", visible); // @todo: think about a PasswordField that automatically hashes on save? } @@ -250,7 +255,10 @@ export class AppAuth extends Module { static usersFields = { email: text().required(), - strategy: text({ fillable: ["create"], hidden: ["form"] }).required(), + strategy: text({ + fillable: ["create"], + hidden: ["update", "form"] + }).required(), strategy_value: text({ fillable: ["create"], hidden: ["read", "table", "update", "form"] diff --git a/app/src/data/entities/EntityManager.ts b/app/src/data/entities/EntityManager.ts index f8dfd7b..31401b3 100644 --- a/app/src/data/entities/EntityManager.ts +++ b/app/src/data/entities/EntityManager.ts @@ -111,13 +111,13 @@ export class EntityManager { // caused issues because this.entity() was using a reference (for when initial config was given) } - entity(e: Entity | keyof TBD | string): Entity { + entity(e: Entity | keyof TBD | string, silent?: boolean): Entity { // make sure to always retrieve by name const entity = this.entities.find((entity) => e instanceof Entity ? entity.name === e.name : entity.name === e ); - if (!entity) { + if (!entity && !silent) { // @ts-ignore throw new EntityNotDefinedException(e instanceof Entity ? e.name : e); } diff --git a/app/src/data/fields/Field.ts b/app/src/data/fields/Field.ts index ffa7f08..f412f5e 100644 --- a/app/src/data/fields/Field.ts +++ b/app/src/data/fields/Field.ts @@ -12,6 +12,9 @@ import type { HTMLInputTypeAttribute, InputHTMLAttributes } from "react"; import type { EntityManager } from "../entities"; import { InvalidFieldConfigException, TransformPersistFailedException } from "../errors"; +// @todo: contexts need to be reworked +// e.g. "table" is irrelevant, because if read is not given, it fails + export const ActionContext = ["create", "read", "update", "delete"] as const; export type TActionContext = (typeof ActionContext)[number]; diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index 546db48..0d5b8bf 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -14,6 +14,7 @@ import { } from "data"; import { Entity } from "data"; import type { Hono } from "hono"; +import { isEqual } from "lodash-es"; export type ServerEnv = { Variables: { @@ -154,28 +155,33 @@ export abstract class Module 0) { + this.ctx.flags.sync_required = true; + } } } // replace entity (mainly to keep the ensured type) this.ctx.em.__replaceEntity( - new Entity(entity.name, entity.fields, instance.config, entity.type) + new Entity(instance.name, instance.fields, instance.config, entity.type) ); } @@ -193,6 +199,21 @@ export abstract class Module = { + id?: number; version: number; type: "config" | "diff" | "backup"; json: Json; @@ -236,10 +237,10 @@ export class ModuleManager { private async fetch(): Promise { this.logger.context("fetch").log("fetching"); + const startTime = performance.now(); // disabling console log, because the table might not exist yet - return await withDisabledConsole(async () => { - const startTime = performance.now(); + const result = await withDisabledConsole(async () => { const { data: result } = await this.repo().findOne( { type: "config" }, { @@ -251,9 +252,16 @@ export class ModuleManager { throw BkndError.with("no config"); } - this.logger.log("took", performance.now() - startTime, "ms", result.version).clear(); - return result as ConfigTable; + return result as unknown as ConfigTable; }, ["log", "error", "warn"]); + + this.logger + .log("took", performance.now() - startTime, "ms", { + version: result.version, + id: result.id + }) + .clear(); + return result; } async save() { @@ -390,6 +398,7 @@ export class ModuleManager { } private setConfigs(configs: ModuleConfigs): void { + this.logger.log("setting configs"); objectEach(configs, (config, key) => { try { // setting "noEmit" to true, to not force listeners to update diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index 8fd50cc..be2e548 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -44,6 +44,11 @@ export class SystemController extends Controller { hono.use(permission(SystemPermissions.configRead)); + hono.get("/raw", permission([SystemPermissions.configReadSecrets]), async (c) => { + // @ts-expect-error "fetch" is private + return c.json(await this.app.modules.fetch()); + }); + hono.get( "/:module?", tb("param", Type.Object({ module: Type.Optional(StringEnum(MODULE_NAMES)) })), diff --git a/app/src/ui/main.css b/app/src/ui/main.css index c6254b0..9968b67 100644 --- a/app/src/ui/main.css +++ b/app/src/ui/main.css @@ -54,9 +54,6 @@ body, } } -@layer utilities { -} - #bknd-admin, .bknd-admin { /* Chrome, Edge, and Safari */