mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
strengthened schema ensuring for system entities
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 extends keyof Modules>(module: Module) {
|
||||
|
||||
@@ -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,7 +224,8 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -235,6 +236,10 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
||||
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<typeof authConfigSchema> {
|
||||
|
||||
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"]
|
||||
|
||||
@@ -111,13 +111,13 @@ export class EntityManager<TBD extends object = DefaultDB> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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<Schema extends TSchema = TSchema, ConfigSchema = St
|
||||
}
|
||||
|
||||
protected ensureEntity(entity: Entity) {
|
||||
const instance = this.ctx.em.entity(entity.name, true);
|
||||
|
||||
// check fields
|
||||
if (!this.ctx.em.hasEntity(entity.name)) {
|
||||
if (!instance) {
|
||||
this.ctx.em.addEntity(entity);
|
||||
this.ctx.flags.sync_required = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = this.ctx.em.entity(entity.name);
|
||||
|
||||
// if exists, check all fields required are there
|
||||
// @todo: check if the field also equal
|
||||
for (const field of instance.fields) {
|
||||
const _field = entity.field(field.name);
|
||||
if (!_field) {
|
||||
entity.addField(field);
|
||||
for (const field of entity.fields) {
|
||||
const instanceField = instance.field(field.name);
|
||||
if (!instanceField) {
|
||||
instance.addField(field);
|
||||
this.ctx.flags.sync_required = true;
|
||||
} else {
|
||||
const changes = this.setEntityFieldConfigs(field, instanceField);
|
||||
if (changes > 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<Schema extends TSchema = TSchema, ConfigSchema = St
|
||||
return schema;
|
||||
}
|
||||
|
||||
protected setEntityFieldConfigs(
|
||||
parent: Field,
|
||||
child: Field,
|
||||
props: string[] = ["hidden", "fillable", "required"]
|
||||
) {
|
||||
let changes = 0;
|
||||
for (const prop of props) {
|
||||
if (!isEqual(child.config[prop], parent.config[prop])) {
|
||||
child.config[prop] = parent.config[prop];
|
||||
changes++;
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
protected replaceEntityField(
|
||||
_entity: string | Entity,
|
||||
field: Field | string,
|
||||
@@ -202,6 +223,10 @@ export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = St
|
||||
const name = typeof field === "string" ? field : field.name;
|
||||
const newField =
|
||||
_newField instanceof FieldPrototype ? make(name, _newField as any) : _newField;
|
||||
|
||||
// ensure keeping vital config
|
||||
this.setEntityFieldConfigs(entity.field(name)!, newField);
|
||||
|
||||
entity.__replaceField(name, newField);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export type ModuleManagerOptions = {
|
||||
};
|
||||
|
||||
type ConfigTable<Json = ModuleConfigs> = {
|
||||
id?: number;
|
||||
version: number;
|
||||
type: "config" | "diff" | "backup";
|
||||
json: Json;
|
||||
@@ -236,10 +237,10 @@ export class ModuleManager {
|
||||
|
||||
private async fetch(): Promise<ConfigTable> {
|
||||
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
|
||||
|
||||
@@ -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)) })),
|
||||
|
||||
@@ -54,9 +54,6 @@ body,
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
}
|
||||
|
||||
#bknd-admin,
|
||||
.bknd-admin {
|
||||
/* Chrome, Edge, and Safari */
|
||||
|
||||
Reference in New Issue
Block a user