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:
dswbx
2025-01-10 14:43:39 +01:00
parent 475563b5e1
commit a8c20d3675
11 changed files with 413 additions and 109 deletions

View 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)
});