diff --git a/app/__test__/app/code-only.test.ts b/app/__test__/app/code-only.test.ts new file mode 100644 index 0000000..26fb8e9 --- /dev/null +++ b/app/__test__/app/code-only.test.ts @@ -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: + expect(app.modules.hasOwnProperty("mutateConfigSafe")).toBe(false); + expect(() => { + app.modules.configs().auth.enabled = true; + }).toThrow(); + }); +}); diff --git a/app/src/cli/commands/run/platform.ts b/app/src/cli/commands/run/platform.ts index c4c2d69..ed2e1aa 100644 --- a/app/src/cli/commands/run/platform.ts +++ b/app/src/cli/commands/run/platform.ts @@ -76,6 +76,9 @@ export async function getConfigPath(filePath?: string) { const config_path = path.resolve(process.cwd(), filePath); if (await fileExists(config_path)) { return config_path; + } else { + $console.error(`Config file could not be resolved: ${config_path}`); + process.exit(1); } } diff --git a/app/src/cli/commands/sync.ts b/app/src/cli/commands/sync.ts index d006efe..8b8c5c4 100644 --- a/app/src/cli/commands/sync.ts +++ b/app/src/cli/commands/sync.ts @@ -8,6 +8,7 @@ export const sync: CliCommand = (program) => { withConfigOptions(program.command("sync")) .description("sync database") .option("--force", "perform database syncing operations") + .option("--seed", "perform seeding operations") .option("--drop", "include destructive DDL operations") .option("--out ", "output file") .option("--sql", "use sql output") @@ -29,8 +30,22 @@ export const sync: CliCommand = (program) => { console.info(c.dim("Executing:") + "\n" + c.cyan(sql)); 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")}`); + + 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 { if (options.out) { const output = options.sql ? sql : JSON.stringify(stmts, null, 2); diff --git a/app/src/cli/index.ts b/app/src/cli/index.ts index 352cdd4..3385713 100644 --- a/app/src/cli/index.ts +++ b/app/src/cli/index.ts @@ -7,7 +7,7 @@ import { getVersion } from "./utils/sys"; import { capture, flush, init } from "cli/utils/telemetry"; const program = new Command(); -export async function main() { +async function main() { await init(); capture("start"); diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index 82c447d..c2ffd3a 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -6,6 +6,7 @@ import { SecretSchema, setPath, mark, + $console, } from "bknd/utils"; import { DebugLogger } from "core/utils/DebugLogger"; import { Guard } from "auth/authorize/Guard"; @@ -126,7 +127,7 @@ export class ModuleManager { constructor( protected readonly connection: Connection, - protected options?: Partial, + public options?: Partial, ) { this.modules = {} as Modules; this.emgr = new EventManager({ ...ModuleManagerEvents }); @@ -330,9 +331,8 @@ export class ModuleManager { ctx.flags.sync_required = false; this.logger.log("db sync requested"); - // sync db - await ctx.em.schema().sync({ force: true, drop: options?.drop }); - state.synced = true; + // sync db hint + $console.warn("a database sync is required"); } if (ctx.flags.ctx_reload_required) {