mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Refactor module schema handling and add sync mechanism
Redesigned entity and index management with methods to streamline schema updates and added a sync flag to signal required DB syncs post-build. Enhanced test coverage and functionality for schema modifications, including support for additional fields.
This commit is contained in:
@@ -1,6 +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 { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||
@@ -102,4 +103,33 @@ describe("AppAuth", () => {
|
||||
|
||||
expect(spy.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
test("should allow additional user fields", async () => {
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
auth: {
|
||||
entity_name: "users",
|
||||
enabled: true
|
||||
},
|
||||
data: em({
|
||||
users: entity("users", {
|
||||
additional: text()
|
||||
})
|
||||
}).toJSON()
|
||||
}
|
||||
});
|
||||
|
||||
await app.build();
|
||||
|
||||
const userfields = app.modules.em.entity("users").fields.map((f) => f.name);
|
||||
expect(userfields).toContain("additional");
|
||||
expect(userfields).toEqual([
|
||||
"id",
|
||||
"additional",
|
||||
"email",
|
||||
"strategy",
|
||||
"strategy_value",
|
||||
"role"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,53 @@
|
||||
import { describe } from "bun:test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
import { em, entity, text } from "../../src/data";
|
||||
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
|
||||
import { AppMedia } from "../../src/modules";
|
||||
import { moduleTestSuite } from "./module-test-suite";
|
||||
|
||||
describe("AppMedia", () => {
|
||||
moduleTestSuite(AppMedia);
|
||||
|
||||
test("should allow additional fields", async () => {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
media: {
|
||||
entity_name: "media",
|
||||
enabled: true,
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: {
|
||||
path: "./"
|
||||
}
|
||||
}
|
||||
},
|
||||
data: em({
|
||||
media: entity("media", {
|
||||
additional: text()
|
||||
})
|
||||
}).toJSON()
|
||||
}
|
||||
});
|
||||
|
||||
await app.build();
|
||||
|
||||
const fields = app.modules.em.entity("media").fields.map((f) => f.name);
|
||||
expect(fields).toContain("additional");
|
||||
expect(fields).toEqual([
|
||||
"id",
|
||||
"additional",
|
||||
"path",
|
||||
"folder",
|
||||
"mime_type",
|
||||
"size",
|
||||
"scope",
|
||||
"etag",
|
||||
"modified_at",
|
||||
"reference",
|
||||
"entity_id",
|
||||
"metadata"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
200
app/__test__/modules/Module.spec.ts
Normal file
200
app/__test__/modules/Module.spec.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type TSchema, Type, stripMark } from "../../src/core/utils";
|
||||
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
import { Module } from "../../src/modules/Module";
|
||||
|
||||
function createModule<Schema extends TSchema>(schema: Schema) {
|
||||
class TestModule extends Module<typeof schema> {
|
||||
getSchema() {
|
||||
return schema;
|
||||
}
|
||||
toJSON() {
|
||||
return this.config;
|
||||
}
|
||||
useForceParse() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return TestModule;
|
||||
}
|
||||
|
||||
describe("Module", async () => {
|
||||
describe("basic", () => {
|
||||
test("listener", async () => {
|
||||
let result: any;
|
||||
|
||||
const module = createModule(Type.Object({ a: Type.String() }));
|
||||
const m = new module({ a: "test" });
|
||||
|
||||
await m.schema().set({ a: "test2" });
|
||||
m.setListener(async (c) => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
result = stripMark(c);
|
||||
});
|
||||
await m.schema().set({ a: "test3" });
|
||||
expect(result).toEqual({ a: "test3" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("db schema", () => {
|
||||
class M extends Module {
|
||||
override getSchema() {
|
||||
return Type.Object({});
|
||||
}
|
||||
|
||||
prt = {
|
||||
ensureEntity: this.ensureEntity.bind(this),
|
||||
ensureIndex: this.ensureIndex.bind(this),
|
||||
ensureSchema: this.ensureSchema.bind(this)
|
||||
};
|
||||
|
||||
get em() {
|
||||
return this.ctx.em;
|
||||
}
|
||||
}
|
||||
|
||||
function make(_em: ReturnType<typeof em>) {
|
||||
const em = new EntityManager(
|
||||
Object.values(_em.entities),
|
||||
new DummyConnection(),
|
||||
_em.relations,
|
||||
_em.indices
|
||||
);
|
||||
return new M({} as any, { em, flags: Module.ctx_flags } as any);
|
||||
}
|
||||
function flat(_em: EntityManager) {
|
||||
return {
|
||||
entities: _em.entities.map((e) => ({
|
||||
name: e.name,
|
||||
fields: e.fields.map((f) => f.name)
|
||||
})),
|
||||
indices: _em.indices.map((i) => ({
|
||||
name: i.name,
|
||||
entity: i.entity.name,
|
||||
fields: i.fields.map((f) => f.name),
|
||||
unique: i.unique
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
test("no change", () => {
|
||||
const initial = em({});
|
||||
|
||||
const m = make(initial);
|
||||
expect(m.ctx.flags.sync_required).toBe(false);
|
||||
|
||||
expect(flat(make(initial).em)).toEqual({
|
||||
entities: [],
|
||||
indices: []
|
||||
});
|
||||
});
|
||||
|
||||
test("init", () => {
|
||||
const initial = em({
|
||||
users: entity("u", {
|
||||
name: text()
|
||||
})
|
||||
});
|
||||
|
||||
const m = make(initial);
|
||||
expect(m.ctx.flags.sync_required).toBe(false);
|
||||
|
||||
expect(flat(m.em)).toEqual({
|
||||
entities: [
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name"]
|
||||
}
|
||||
],
|
||||
indices: []
|
||||
});
|
||||
});
|
||||
|
||||
test("ensure entity", () => {
|
||||
const initial = em({
|
||||
users: entity("u", {
|
||||
name: text()
|
||||
})
|
||||
});
|
||||
|
||||
const m = make(initial);
|
||||
expect(flat(m.em)).toEqual({
|
||||
entities: [
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name"]
|
||||
}
|
||||
],
|
||||
indices: []
|
||||
});
|
||||
|
||||
// this should add a new entity
|
||||
m.prt.ensureEntity(
|
||||
entity("p", {
|
||||
title: text()
|
||||
})
|
||||
);
|
||||
|
||||
// this should only add the field "important"
|
||||
m.prt.ensureEntity(
|
||||
entity("u", {
|
||||
important: text()
|
||||
})
|
||||
);
|
||||
|
||||
expect(m.ctx.flags.sync_required).toBe(true);
|
||||
expect(flat(m.em)).toEqual({
|
||||
entities: [
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name", "important"]
|
||||
},
|
||||
{
|
||||
name: "p",
|
||||
fields: ["id", "title"]
|
||||
}
|
||||
],
|
||||
indices: []
|
||||
});
|
||||
});
|
||||
|
||||
test("ensure index", () => {
|
||||
const users = entity("u", {
|
||||
name: text(),
|
||||
title: text()
|
||||
});
|
||||
const initial = em({ users }, ({ index }, { users }) => {
|
||||
index(users).on(["title"]);
|
||||
});
|
||||
|
||||
const m = make(initial);
|
||||
m.prt.ensureIndex(index(users).on(["name"]));
|
||||
|
||||
expect(m.ctx.flags.sync_required).toBe(true);
|
||||
expect(flat(m.em)).toEqual({
|
||||
entities: [
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name", "title"]
|
||||
}
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
name: "idx_u_title",
|
||||
entity: "u",
|
||||
fields: ["title"],
|
||||
unique: false
|
||||
},
|
||||
{
|
||||
name: "idx_u_name",
|
||||
entity: "u",
|
||||
fields: ["name"],
|
||||
unique: false
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
255
app/__test__/modules/ModuleManager.spec.ts
Normal file
255
app/__test__/modules/ModuleManager.spec.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { stripMark } from "../../src/core/utils";
|
||||
import { entity, text } from "../../src/data";
|
||||
import { ModuleManager, getDefaultConfig } from "../../src/modules/ModuleManager";
|
||||
import { CURRENT_VERSION, TABLE_NAME } from "../../src/modules/migrations";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
describe("ModuleManager", async () => {
|
||||
test("s1: no config, no build", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const mm = new ModuleManager(dummyConnection);
|
||||
|
||||
// that is because no module is built
|
||||
expect(mm.toJSON()).toEqual({ version: 0 } as any);
|
||||
});
|
||||
|
||||
test("s2: no config, build", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const mm = new ModuleManager(dummyConnection);
|
||||
await mm.build();
|
||||
|
||||
expect(mm.version()).toBe(CURRENT_VERSION);
|
||||
expect(mm.built()).toBe(true);
|
||||
});
|
||||
|
||||
test("s3: config given, table exists, version matches", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const version = mm.version();
|
||||
const configs = mm.configs();
|
||||
const json = stripMark({
|
||||
...configs,
|
||||
data: {
|
||||
...configs.data,
|
||||
basepath: "/api/data2",
|
||||
entities: {
|
||||
test: entity("test", {
|
||||
content: text()
|
||||
}).toJSON()
|
||||
}
|
||||
}
|
||||
});
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, { initial: { version, ...json } });
|
||||
await mm2.syncConfigTable();
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({ type: "config", json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||
.execute();
|
||||
|
||||
await mm2.build();
|
||||
|
||||
expect(json).toEqual(stripMark(mm2.configs()));
|
||||
});
|
||||
|
||||
test("s3.1: (fetch) config given, table exists, version matches", async () => {
|
||||
const configs = getDefaultConfig();
|
||||
const json = {
|
||||
...configs,
|
||||
data: {
|
||||
...configs.data,
|
||||
basepath: "/api/data2",
|
||||
entities: {
|
||||
test: entity("test", {
|
||||
content: text()
|
||||
}).toJSON()
|
||||
}
|
||||
}
|
||||
};
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
const db = dummyConnection.kysely;
|
||||
const mm2 = new ModuleManager(dummyConnection);
|
||||
await mm2.syncConfigTable();
|
||||
// assume an initial version
|
||||
await db.insertInto(TABLE_NAME).values({ type: "config", json: null, version: 1 }).execute();
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({ type: "config", json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||
.execute();
|
||||
|
||||
await mm2.build();
|
||||
|
||||
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||
expect(mm2.configs().data.entities.test).toBeDefined();
|
||||
expect(mm2.configs().data.entities.test.fields.content).toBeDefined();
|
||||
expect(mm2.get("data").toJSON().entities.test.fields.content).toBeDefined();
|
||||
});
|
||||
|
||||
test("s4: config given, table exists, version outdated, migrate", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const version = mm.version();
|
||||
const json = mm.configs();
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json }
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({ json: JSON.stringify(json), type: "config", version: CURRENT_VERSION - 1 })
|
||||
.execute();
|
||||
|
||||
await mm2.build();
|
||||
});
|
||||
|
||||
test("s5: config given, table exists, version mismatch", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const version = mm.version();
|
||||
const json = mm.configs();
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: { version: version - 1, ...json }
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({ type: "config", json: JSON.stringify(json), version: CURRENT_VERSION })
|
||||
.execute();
|
||||
|
||||
expect(mm2.build()).rejects.toThrow(/version.*do not match/);
|
||||
});
|
||||
|
||||
test("s6: no config given, table exists, fetch", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const json = mm.configs();
|
||||
//const { version, ...json } = mm.toJSON() as any;
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection);
|
||||
await mm2.syncConfigTable();
|
||||
|
||||
const config = {
|
||||
...json,
|
||||
data: {
|
||||
...json.data,
|
||||
basepath: "/api/data2"
|
||||
}
|
||||
};
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({ type: "config", json: JSON.stringify(config), version: CURRENT_VERSION })
|
||||
.execute();
|
||||
|
||||
// run without config given
|
||||
await mm2.build();
|
||||
|
||||
expect(mm2.configs().data.basepath).toBe("/api/data2");
|
||||
});
|
||||
|
||||
test("blank app, modify config", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const mm = new ModuleManager(dummyConnection);
|
||||
await mm.build();
|
||||
const configs = stripMark(mm.configs());
|
||||
|
||||
expect(mm.configs().server.admin.color_scheme).toBe("light");
|
||||
expect(() => mm.get("server").schema().patch("admin", { color_scheme: "violet" })).toThrow();
|
||||
await mm.get("server").schema().patch("admin", { color_scheme: "dark" });
|
||||
await mm.save();
|
||||
|
||||
expect(mm.configs().server.admin.color_scheme).toBe("dark");
|
||||
expect(stripMark(mm.configs())).toEqual({
|
||||
...configs,
|
||||
server: {
|
||||
...configs.server,
|
||||
admin: {
|
||||
...configs.server.admin,
|
||||
color_scheme: "dark"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("partial config given", async () => {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
|
||||
const partial = {
|
||||
auth: {
|
||||
enabled: true
|
||||
}
|
||||
};
|
||||
const mm = new ModuleManager(dummyConnection, {
|
||||
initial: partial
|
||||
});
|
||||
await mm.build();
|
||||
|
||||
expect(mm.version()).toBe(CURRENT_VERSION);
|
||||
expect(mm.built()).toBe(true);
|
||||
expect(mm.configs().auth.enabled).toBe(true);
|
||||
expect(mm.configs().data.entities.users).toBeDefined();
|
||||
});
|
||||
|
||||
test("partial config given, but db version exists", async () => {
|
||||
const c = getDummyConnection();
|
||||
const mm = new ModuleManager(c.dummyConnection);
|
||||
await mm.build();
|
||||
const json = mm.configs();
|
||||
|
||||
const c2 = getDummyConnection();
|
||||
const db = c2.dummyConnection.kysely;
|
||||
|
||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||
initial: {
|
||||
auth: {
|
||||
basepath: "/shouldnt/take/this"
|
||||
}
|
||||
}
|
||||
});
|
||||
await mm2.syncConfigTable();
|
||||
const payload = {
|
||||
...json,
|
||||
auth: {
|
||||
...json.auth,
|
||||
enabled: true,
|
||||
basepath: "/api/auth2"
|
||||
}
|
||||
};
|
||||
await db
|
||||
.insertInto(TABLE_NAME)
|
||||
.values({
|
||||
type: "config",
|
||||
json: JSON.stringify(payload),
|
||||
version: CURRENT_VERSION
|
||||
})
|
||||
.execute();
|
||||
await mm2.build();
|
||||
expect(mm2.configs().auth.basepath).toBe("/api/auth2");
|
||||
});
|
||||
|
||||
// @todo: add tests for migrations (check "backup" and new version)
|
||||
});
|
||||
Reference in New Issue
Block a user