mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
feat: add code-only tests and enhance CLI sync command with seeding option
Introduced a new test suite for code-only applications, validating app creation, database sync behavior, and seeding functionality. Enhanced the CLI sync command to include a seeding option, allowing for explicit seeding during database synchronization. Added error handling for unresolved config files in the run command.
This commit is contained in:
127
app/__test__/app/code-only.test.ts
Normal file
127
app/__test__/app/code-only.test.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
|
import { createApp as internalCreateApp, type CreateAppConfig } from "bknd";
|
||||||
|
import { getDummyConnection } from "../../__test__/helper";
|
||||||
|
import { ModuleManager } from "modules/ModuleManager";
|
||||||
|
import { em, entity, text } from "data/prototype";
|
||||||
|
|
||||||
|
async function createApp(config: CreateAppConfig = {}) {
|
||||||
|
const app = internalCreateApp({
|
||||||
|
connection: getDummyConnection().dummyConnection,
|
||||||
|
...config,
|
||||||
|
options: {
|
||||||
|
...config.options,
|
||||||
|
mode: "code",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("code-only", () => {
|
||||||
|
test("should create app with correct manager", async () => {
|
||||||
|
const app = await createApp();
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
expect(app.version()).toBeDefined();
|
||||||
|
expect(app.modules).toBeInstanceOf(ModuleManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not perform database syncs", async () => {
|
||||||
|
const app = await createApp({
|
||||||
|
config: {
|
||||||
|
data: em({
|
||||||
|
test: entity("test", {
|
||||||
|
name: text(),
|
||||||
|
}),
|
||||||
|
}).toJSON(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(app.em.entities.map((e) => e.name)).toEqual(["test"]);
|
||||||
|
expect(
|
||||||
|
await app.em.connection.kysely
|
||||||
|
.selectFrom("sqlite_master")
|
||||||
|
.where("type", "=", "table")
|
||||||
|
.selectAll()
|
||||||
|
.execute(),
|
||||||
|
).toEqual([]);
|
||||||
|
|
||||||
|
// only perform when explicitly forced
|
||||||
|
await app.em.schema().sync({ force: true });
|
||||||
|
expect(
|
||||||
|
await app.em.connection.kysely
|
||||||
|
.selectFrom("sqlite_master")
|
||||||
|
.where("type", "=", "table")
|
||||||
|
.selectAll()
|
||||||
|
.execute()
|
||||||
|
.then((r) => r.map((r) => r.name)),
|
||||||
|
).toEqual(["test", "sqlite_sequence"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not perform seeding", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
const app = await createApp({
|
||||||
|
config: {
|
||||||
|
data: em({
|
||||||
|
test: entity("test", {
|
||||||
|
name: text(),
|
||||||
|
}),
|
||||||
|
}).toJSON(),
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
seed: async (ctx) => {
|
||||||
|
called();
|
||||||
|
await ctx.em.mutator("test").insertOne({ name: "test" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.em.schema().sync({ force: true });
|
||||||
|
expect(called).not.toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
await app.em
|
||||||
|
.repo("test")
|
||||||
|
.findMany({})
|
||||||
|
.then((r) => r.data),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should sync and perform seeding", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
const app = await createApp({
|
||||||
|
config: {
|
||||||
|
data: em({
|
||||||
|
test: entity("test", {
|
||||||
|
name: text(),
|
||||||
|
}),
|
||||||
|
}).toJSON(),
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
seed: async (ctx) => {
|
||||||
|
called();
|
||||||
|
await ctx.em.mutator("test").insertOne({ name: "test" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.em.schema().sync({ force: true });
|
||||||
|
await app.options?.seed?.({
|
||||||
|
...app.modules.ctx(),
|
||||||
|
app: app,
|
||||||
|
});
|
||||||
|
expect(called).toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
await app.em
|
||||||
|
.repo("test")
|
||||||
|
.findMany({})
|
||||||
|
.then((r) => r.data),
|
||||||
|
).toEqual([{ id: 1, name: "test" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not allow to modify config", async () => {
|
||||||
|
const app = await createApp();
|
||||||
|
// biome-ignore lint/suspicious/noPrototypeBuiltins: <explanation>
|
||||||
|
expect(app.modules.hasOwnProperty("mutateConfigSafe")).toBe(false);
|
||||||
|
expect(() => {
|
||||||
|
app.modules.configs().auth.enabled = true;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -76,6 +76,9 @@ export async function getConfigPath(filePath?: string) {
|
|||||||
const config_path = path.resolve(process.cwd(), filePath);
|
const config_path = path.resolve(process.cwd(), filePath);
|
||||||
if (await fileExists(config_path)) {
|
if (await fileExists(config_path)) {
|
||||||
return config_path;
|
return config_path;
|
||||||
|
} else {
|
||||||
|
$console.error(`Config file could not be resolved: ${config_path}`);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const sync: CliCommand = (program) => {
|
|||||||
withConfigOptions(program.command("sync"))
|
withConfigOptions(program.command("sync"))
|
||||||
.description("sync database")
|
.description("sync database")
|
||||||
.option("--force", "perform database syncing operations")
|
.option("--force", "perform database syncing operations")
|
||||||
|
.option("--seed", "perform seeding operations")
|
||||||
.option("--drop", "include destructive DDL operations")
|
.option("--drop", "include destructive DDL operations")
|
||||||
.option("--out <file>", "output file")
|
.option("--out <file>", "output file")
|
||||||
.option("--sql", "use sql output")
|
.option("--sql", "use sql output")
|
||||||
@@ -29,8 +30,22 @@ export const sync: CliCommand = (program) => {
|
|||||||
console.info(c.dim("Executing:") + "\n" + c.cyan(sql));
|
console.info(c.dim("Executing:") + "\n" + c.cyan(sql));
|
||||||
await schema.sync({ force: true, drop: options.drop });
|
await schema.sync({ force: true, drop: options.drop });
|
||||||
|
|
||||||
console.info(`\n${c.gray(`Executed ${c.cyan(stmts.length)} statement(s)`)}`);
|
console.info(`\n${c.dim(`Executed ${c.cyan(stmts.length)} statement(s)`)}`);
|
||||||
console.info(`${c.green("Database synced")}`);
|
console.info(`${c.green("Database synced")}`);
|
||||||
|
|
||||||
|
if (options.seed) {
|
||||||
|
console.info(c.dim("\nExecuting seed..."));
|
||||||
|
const seed = app.options?.seed;
|
||||||
|
if (seed) {
|
||||||
|
await app.options?.seed?.({
|
||||||
|
...app.modules.ctx(),
|
||||||
|
app: app,
|
||||||
|
});
|
||||||
|
console.info(c.green("Seed executed"));
|
||||||
|
} else {
|
||||||
|
console.info(c.yellow("No seed function provided"));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (options.out) {
|
if (options.out) {
|
||||||
const output = options.sql ? sql : JSON.stringify(stmts, null, 2);
|
const output = options.sql ? sql : JSON.stringify(stmts, null, 2);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getVersion } from "./utils/sys";
|
|||||||
import { capture, flush, init } from "cli/utils/telemetry";
|
import { capture, flush, init } from "cli/utils/telemetry";
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
export async function main() {
|
async function main() {
|
||||||
await init();
|
await init();
|
||||||
capture("start");
|
capture("start");
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
SecretSchema,
|
SecretSchema,
|
||||||
setPath,
|
setPath,
|
||||||
mark,
|
mark,
|
||||||
|
$console,
|
||||||
} from "bknd/utils";
|
} from "bknd/utils";
|
||||||
import { DebugLogger } from "core/utils/DebugLogger";
|
import { DebugLogger } from "core/utils/DebugLogger";
|
||||||
import { Guard } from "auth/authorize/Guard";
|
import { Guard } from "auth/authorize/Guard";
|
||||||
@@ -126,7 +127,7 @@ export class ModuleManager {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly connection: Connection,
|
protected readonly connection: Connection,
|
||||||
protected options?: Partial<ModuleManagerOptions>,
|
public options?: Partial<ModuleManagerOptions>,
|
||||||
) {
|
) {
|
||||||
this.modules = {} as Modules;
|
this.modules = {} as Modules;
|
||||||
this.emgr = new EventManager({ ...ModuleManagerEvents });
|
this.emgr = new EventManager({ ...ModuleManagerEvents });
|
||||||
@@ -330,9 +331,8 @@ export class ModuleManager {
|
|||||||
ctx.flags.sync_required = false;
|
ctx.flags.sync_required = false;
|
||||||
this.logger.log("db sync requested");
|
this.logger.log("db sync requested");
|
||||||
|
|
||||||
// sync db
|
// sync db hint
|
||||||
await ctx.em.schema().sync({ force: true, drop: options?.drop });
|
$console.warn("a database sync is required");
|
||||||
state.synced = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.flags.ctx_reload_required) {
|
if (ctx.flags.ctx_reload_required) {
|
||||||
|
|||||||
Reference in New Issue
Block a user