Refactor entity handling to preserve config while overriding type

Reworked `ensureEntity` to replace entities while maintaining their configuration and allowing type adjustments. Updated tests to verify type persistence and synchronization of entity properties.
This commit is contained in:
dswbx
2025-01-10 15:51:47 +01:00
parent 1d5f14fae0
commit e94e8d8bd1
10 changed files with 63 additions and 41 deletions

View File

@@ -121,15 +121,10 @@ describe("AppAuth", () => {
await app.build(); await app.build();
const userfields = app.modules.em.entity("users").fields.map((f) => f.name); const e = app.modules.em.entity("users");
expect(userfields).toContain("additional"); const fields = e.fields.map((f) => f.name);
expect(userfields).toEqual([ expect(e.type).toBe("system");
"id", expect(fields).toContain("additional");
"additional", expect(fields).toEqual(["id", "email", "strategy", "strategy_value", "role", "additional"]);
"email",
"strategy",
"strategy_value",
"role"
]);
}); });
}); });

View File

@@ -33,11 +33,12 @@ describe("AppMedia", () => {
await app.build(); await app.build();
const fields = app.modules.em.entity("media").fields.map((f) => f.name); const e = app.modules.em.entity("media");
const fields = e.fields.map((f) => f.name);
expect(e.type).toBe("system");
expect(fields).toContain("additional"); expect(fields).toContain("additional");
expect(fields).toEqual([ expect(fields).toEqual([
"id", "id",
"additional",
"path", "path",
"folder", "folder",
"mime_type", "mime_type",
@@ -47,7 +48,8 @@ describe("AppMedia", () => {
"modified_at", "modified_at",
"reference", "reference",
"entity_id", "entity_id",
"metadata" "metadata",
"additional"
]); ]);
}); });
}); });

View File

@@ -68,7 +68,8 @@ describe("Module", async () => {
return { return {
entities: _em.entities.map((e) => ({ entities: _em.entities.map((e) => ({
name: e.name, name: e.name,
fields: e.fields.map((f) => f.name) fields: e.fields.map((f) => f.name),
type: e.type
})), })),
indices: _em.indices.map((i) => ({ indices: _em.indices.map((i) => ({
name: i.name, name: i.name,
@@ -105,7 +106,8 @@ describe("Module", async () => {
entities: [ entities: [
{ {
name: "u", name: "u",
fields: ["id", "name"] fields: ["id", "name"],
type: "regular"
} }
], ],
indices: [] indices: []
@@ -124,7 +126,8 @@ describe("Module", async () => {
entities: [ entities: [
{ {
name: "u", name: "u",
fields: ["id", "name"] fields: ["id", "name"],
type: "regular"
} }
], ],
indices: [] indices: []
@@ -139,9 +142,14 @@ describe("Module", async () => {
// this should only add the field "important" // this should only add the field "important"
m.prt.ensureEntity( m.prt.ensureEntity(
entity("u", { entity(
important: text() "u",
}) {
important: text()
},
undefined,
"system"
)
); );
expect(m.ctx.flags.sync_required).toBe(true); expect(m.ctx.flags.sync_required).toBe(true);
@@ -149,11 +157,15 @@ describe("Module", async () => {
entities: [ entities: [
{ {
name: "u", name: "u",
fields: ["id", "name", "important"] // ensured properties must come first
fields: ["id", "important", "name"],
// ensured type must be present
type: "system"
}, },
{ {
name: "p", name: "p",
fields: ["id", "title"] fields: ["id", "title"],
type: "regular"
} }
], ],
indices: [] indices: []
@@ -177,7 +189,8 @@ describe("Module", async () => {
entities: [ entities: [
{ {
name: "u", name: "u",
fields: ["id", "name", "title"] fields: ["id", "name", "title"],
type: "regular"
} }
], ],
indices: [ indices: [

View File

@@ -5,7 +5,7 @@ import { Guard } from "../../src/auth";
import { EventManager } from "../../src/core/events"; import { EventManager } from "../../src/core/events";
import { Default, stripMark } from "../../src/core/utils"; import { Default, stripMark } from "../../src/core/utils";
import { EntityManager } from "../../src/data"; import { EntityManager } from "../../src/data";
import type { Module, ModuleBuildContext } from "../../src/modules/Module"; import { Module, type ModuleBuildContext } from "../../src/modules/Module";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext { export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
@@ -16,6 +16,7 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
em: new EntityManager([], dummyConnection), em: new EntityManager([], dummyConnection),
emgr: new EventManager(), emgr: new EventManager(),
guard: new Guard(), guard: new Guard(),
flags: Module.ctx_flags,
...overrides ...overrides
}; };
} }

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.4.0", "version": "0.5.0-rc5",
"scripts": { "scripts": {
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli", "build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
"dev": "vite", "dev": "vite",

View File

@@ -250,13 +250,11 @@ export class AppAuth extends Module<typeof authConfigSchema> {
}; };
registerEntities() { registerEntities() {
const name = this.config.entity_name as "users"; const users = this.getUsersEntity(true);
const { this.ensureSchema(
entities: { users }
} = this.ensureSchema(
em( em(
{ {
[name]: entity(name, AppAuth.usersFields) [users.name as "users"]: users
}, },
({ index }, { users }) => { ({ index }, { users }) => {
index(users).on(["email"], true).on(["strategy"]).on(["strategy_value"]); index(users).on(["email"], true).on(["strategy"]).on(["strategy_value"]);
@@ -267,13 +265,13 @@ export class AppAuth extends Module<typeof authConfigSchema> {
try { try {
const roles = Object.keys(this.config.roles ?? {}); const roles = Object.keys(this.config.roles ?? {});
const field = make("role", enumm({ enum: roles })); const field = make("role", enumm({ enum: roles }));
users.__experimental_replaceField("role", field); users.__replaceField("role", field);
} catch (e) {} } catch (e) {}
try { try {
const strategies = Object.keys(this.config.strategies ?? {}); const strategies = Object.keys(this.config.strategies ?? {});
const field = make("strategy", enumm({ enum: strategies })); const field = make("strategy", enumm({ enum: strategies }));
users.__experimental_replaceField("strategy", field); users.__replaceField("strategy", field);
} catch (e) {} } catch (e) {}
} }

View File

@@ -140,7 +140,7 @@ export class Entity<
return this.fields.find((field) => field.name === name); return this.fields.find((field) => field.name === name);
} }
__experimental_replaceField(name: string, field: Field) { __replaceField(name: string, field: Field) {
const index = this.fields.findIndex((f) => f.name === name); const index = this.fields.findIndex((f) => f.name === name);
if (index === -1) { if (index === -1) {
throw new Error(`Field "${name}" not found on entity "${this.name}"`); throw new Error(`Field "${name}" not found on entity "${this.name}"`);

View File

@@ -99,6 +99,16 @@ export class EntityManager<TBD extends object = DefaultDB> {
this.entities.push(entity); this.entities.push(entity);
} }
__replaceEntity(entity: Entity, name: string | undefined = entity.name) {
const entityIndex = this._entities.findIndex((e) => e.name === name);
if (entityIndex === -1) {
throw new Error(`Entity "${name}" not found and cannot be replaced`);
}
this._entities[entityIndex] = entity;
}
entity(e: Entity | keyof TBD | string): Entity { entity(e: Entity | keyof TBD | string): Entity {
let entity: Entity | undefined; let entity: Entity | undefined;
if (typeof e === "string") { if (typeof e === "string") {

View File

@@ -48,10 +48,9 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
this.setupListeners(); this.setupListeners();
this.ctx.server.route(this.basepath, new MediaController(this).getController()); this.ctx.server.route(this.basepath, new MediaController(this).getController());
const mediaEntity = this.getMediaEntity(true); const media = this.getMediaEntity(true);
const name = mediaEntity.name as "media";
this.ensureSchema( this.ensureSchema(
em({ [name]: mediaEntity }, ({ index }, { media }) => { em({ [media.name as "media"]: media }, ({ index }, { media }) => {
index(media).on(["path"], true).on(["reference"]); index(media).on(["path"], true).on(["reference"]);
}) })
); );

View File

@@ -3,7 +3,8 @@ import type { Guard } from "auth";
import { SchemaObject } from "core"; import { SchemaObject } from "core";
import type { EventManager } from "core/events"; import type { EventManager } from "core/events";
import type { Static, TSchema } from "core/utils"; import type { Static, TSchema } from "core/utils";
import type { Connection, Entity, EntityIndex, EntityManager, em as prototypeEm } from "data"; import type { Connection, EntityIndex, EntityManager, em as prototypeEm } from "data";
import { Entity } from "data";
import type { Hono } from "hono"; import type { Hono } from "hono";
export type ServerEnv = { export type ServerEnv = {
@@ -138,8 +139,6 @@ export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = St
return this.config; return this.config;
} }
// @todo: add a method to signal the requirement of database sync!!!
protected ensureEntity(entity: Entity) { protected ensureEntity(entity: Entity) {
// check fields // check fields
if (!this.ctx.em.hasEntity(entity.name)) { if (!this.ctx.em.hasEntity(entity.name)) {
@@ -152,13 +151,18 @@ export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = St
// if exists, check all fields required are there // if exists, check all fields required are there
// @todo: check if the field also equal // @todo: check if the field also equal
for (const field of entity.fields) { for (const field of instance.fields) {
const _field = instance.field(field.name); const _field = entity.field(field.name);
if (!_field) { if (!_field) {
instance.addField(field); entity.addField(field);
this.ctx.flags.sync_required = true; 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)
);
} }
protected ensureIndex(index: EntityIndex) { protected ensureIndex(index: EntityIndex) {