mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d89fe4894 | |||
| 50e9be833b | |||
| 806b7427c1 | |||
| 8742938074 | |||
| 36e61cab3f | |||
| 17d4adbbfa | |||
| d052871fe0 | |||
| b0e5a49b0b | |||
| 35cbcc221a | |||
| 26d1f2b583 | |||
| 95d114ea68 | |||
| 059becbf09 | |||
| 91120091a3 | |||
| 99a812cc75 | |||
| 29a2de31c6 | |||
| 2688bf261b | |||
| 15c32c0e6d | |||
| d6f2500be2 | |||
| fa8931ad15 | |||
| 6e3060141b | |||
| 07029e3797 | |||
| 3672cc7f2c | |||
| ac32eb128e | |||
| ffdf453fea | |||
| 9dd7432e6b | |||
| 8c4a8d91a2 | |||
| 9aae6e78d6 | |||
| 317b2b50ad | |||
| a0b2dde034 | |||
| 70eaa22327 | |||
| ddfc3e599f | |||
| c11dd2bd9e | |||
| 77c85bfd5c | |||
| 732cd31e1f | |||
| 1a7670f57a | |||
| af573cc79a | |||
| 1df87c8a16 | |||
| 3c5e3f9638 | |||
| 7a0a7481c0 | |||
| bdcc81b2f1 | |||
| 94e168589d | |||
| c22339e4bf | |||
| d01058595f | |||
| fdec5f0693 | |||
| e8f2c70279 | |||
| 2c5371610b | |||
| 758a89b5d7 | |||
| e3888537f9 | |||
| a559a2eabc |
@@ -1,12 +1,15 @@
|
|||||||
import { afterEach, describe, test, expect } from "bun:test";
|
import { afterEach, describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||||
import { App, createApp } from "core/test/utils";
|
import { App, createApp } from "core/test/utils";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import * as proto from "../src/data/prototype";
|
import * as proto from "../src/data/prototype";
|
||||||
import { pick } from "lodash-es";
|
import { pick } from "lodash-es";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterEach(afterAllCleanup);
|
afterEach(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("App tests", async () => {
|
describe("App tests", async () => {
|
||||||
test("boots and pongs", async () => {
|
test("boots and pongs", async () => {
|
||||||
@@ -19,7 +22,7 @@ describe("App tests", async () => {
|
|||||||
test("plugins", async () => {
|
test("plugins", async () => {
|
||||||
const called: string[] = [];
|
const called: string[] = [];
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, describe, it, beforeAll, afterAll } from "bun:test";
|
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
|
||||||
import * as adapter from "adapter";
|
import * as adapter from "adapter";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
@@ -19,50 +19,39 @@ describe("adapter", () => {
|
|||||||
expect(
|
expect(
|
||||||
omitKeys(
|
omitKeys(
|
||||||
await adapter.makeConfig(
|
await adapter.makeConfig(
|
||||||
{ app: (a) => ({ initialConfig: { server: { cors: { origin: a.env.TEST } } } }) },
|
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||||
{ env: { TEST: "test" } },
|
{ env: { TEST: "test" } },
|
||||||
),
|
),
|
||||||
["connection"],
|
["connection"],
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
initialConfig: { server: { cors: { origin: "test" } } },
|
config: { server: { cors: { origin: "test" } } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* it.only("...", async () => {
|
it("allows all properties in app function", async () => {
|
||||||
const app = await adapter.createAdapterApp();
|
const called = mock(() => null);
|
||||||
}); */
|
const config = await adapter.makeConfig(
|
||||||
|
|
||||||
it("reuses apps correctly", async () => {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
|
|
||||||
const first = await adapter.createAdapterApp(
|
|
||||||
{
|
{
|
||||||
initialConfig: { server: { cors: { origin: "random" } } },
|
app: (env) => ({
|
||||||
|
connection: { url: "test" },
|
||||||
|
config: { server: { cors: { origin: "test" } } },
|
||||||
|
options: {
|
||||||
|
mode: "db",
|
||||||
|
},
|
||||||
|
onBuilt: () => {
|
||||||
|
called();
|
||||||
|
expect(env).toEqual({ foo: "bar" });
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
undefined,
|
{ foo: "bar" },
|
||||||
{ id },
|
|
||||||
);
|
);
|
||||||
const second = await adapter.createAdapterApp();
|
expect(config.connection).toEqual({ url: "test" });
|
||||||
const third = await adapter.createAdapterApp(undefined, undefined, { id });
|
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
|
||||||
|
expect(config.options).toEqual({ mode: "db" });
|
||||||
await first.build();
|
await config.onBuilt?.(null as any);
|
||||||
await second.build();
|
expect(called).toHaveBeenCalled();
|
||||||
await third.build();
|
|
||||||
|
|
||||||
expect(first.toJSON().server.cors.origin).toEqual("random");
|
|
||||||
expect(first).toBe(third);
|
|
||||||
expect(first).not.toBe(second);
|
|
||||||
expect(second).not.toBe(third);
|
|
||||||
expect(second.toJSON().server.cors.origin).toEqual("*");
|
|
||||||
|
|
||||||
// recreate the first one
|
|
||||||
const first2 = await adapter.createAdapterApp(undefined, undefined, { id, force: true });
|
|
||||||
await first2.build();
|
|
||||||
expect(first2).not.toBe(first);
|
|
||||||
expect(first2).not.toBe(third);
|
|
||||||
expect(first2).not.toBe(second);
|
|
||||||
expect(first2.toJSON().server.cors.origin).toEqual("*");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
adapterTestSuite(bunTestRunner, {
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ describe("Api", async () => {
|
|||||||
expect(api.isAuthVerified()).toBe(false);
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
|
||||||
const params = api.getParams();
|
const params = api.getParams();
|
||||||
console.log(params);
|
|
||||||
expect(params.token).toBe(token);
|
expect(params.token).toBe(token);
|
||||||
expect(params.token_transport).toBe("cookie");
|
expect(params.token_transport).toBe("cookie");
|
||||||
expect(params.host).toBe("http://example.com");
|
expect(params.host).toBe("http://example.com");
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import { describe, expect, mock, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||||
import type { ModuleBuildContext } from "../../src";
|
import type { ModuleBuildContext } from "../../src";
|
||||||
import { App, createApp } from "core/test/utils";
|
import { App, createApp } from "core/test/utils";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "data/prototype";
|
||||||
|
import { DbModuleManager } from "modules/db/DbModuleManager";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
|
test("use db mode by default", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
expect(app.mode).toBe("db");
|
||||||
|
expect(app.isReadOnly()).toBe(false);
|
||||||
|
expect(app.modules instanceof DbModuleManager).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("seed includes ctx and app", async () => {
|
test("seed includes ctx and app", async () => {
|
||||||
const called = mock(() => null);
|
const called = mock(() => null);
|
||||||
await createApp({
|
await createApp({
|
||||||
@@ -29,7 +43,7 @@ describe("App", () => {
|
|||||||
expect(called).toHaveBeenCalled();
|
expect(called).toHaveBeenCalled();
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
data: proto
|
data: proto
|
||||||
.em({
|
.em({
|
||||||
todos: proto.entity("todos", {
|
todos: proto.entity("todos", {
|
||||||
@@ -139,7 +153,7 @@ describe("App", () => {
|
|||||||
|
|
||||||
test("getMcpClient", async () => {
|
test("getMcpClient", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
server: {
|
server: {
|
||||||
mcp: {
|
mcp: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ describe("mcp auth", async () => {
|
|||||||
let server: McpServer;
|
let server: McpServer;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = createApp({
|
app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
jwt: {
|
jwt: {
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import { registries } from "index";
|
import { registries } from "index";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("mcp", () => {
|
describe("mcp", () => {
|
||||||
it("should have tools", async () => {
|
it("should have tools", async () => {
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe("mcp data", async () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const time = performance.now();
|
const time = performance.now();
|
||||||
app = createApp({
|
app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
server: {
|
server: {
|
||||||
mcp: {
|
mcp: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ describe("mcp media", async () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
app = createApp({
|
app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
media: {
|
media: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
adapter: {
|
adapter: {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, test, expect, beforeAll, mock, beforeEach, afterAll } from "bun:test";
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
||||||
import type { McpServer } from "bknd/utils";
|
import type { McpServer } from "bknd/utils";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - [x] config_server_get
|
* - [x] config_server_get
|
||||||
@@ -11,7 +15,7 @@ describe("mcp system", async () => {
|
|||||||
let server: McpServer;
|
let server: McpServer;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = createApp({
|
app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
server: {
|
server: {
|
||||||
mcp: {
|
mcp: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe("mcp system", async () => {
|
|||||||
let server: McpServer;
|
let server: McpServer;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = createApp({
|
app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
server: {
|
server: {
|
||||||
mcp: {
|
mcp: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { describe, it, expect, mock, beforeAll, afterAll } from "bun:test";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
|
import { syncConfig } from "plugins/dev/sync-config.plugin";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("syncConfig", () => {
|
||||||
|
it("should only sync if enabled", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
const app = createApp();
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
await syncConfig({
|
||||||
|
write: () => {
|
||||||
|
called();
|
||||||
|
},
|
||||||
|
enabled: false,
|
||||||
|
includeFirstBoot: false,
|
||||||
|
})(app).onBuilt?.();
|
||||||
|
expect(called).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await syncConfig({
|
||||||
|
write: () => {
|
||||||
|
called();
|
||||||
|
},
|
||||||
|
enabled: false,
|
||||||
|
includeFirstBoot: true,
|
||||||
|
})(app).onBuilt?.();
|
||||||
|
expect(called).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await syncConfig({
|
||||||
|
write: () => {
|
||||||
|
called();
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
includeFirstBoot: true,
|
||||||
|
})(app).onBuilt?.();
|
||||||
|
expect(called).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should respect secrets", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
await syncConfig({
|
||||||
|
write: async (config) => {
|
||||||
|
called();
|
||||||
|
expect(config.auth.jwt.secret).toBe("test");
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
includeSecrets: true,
|
||||||
|
includeFirstBoot: true,
|
||||||
|
})(app).onBuilt?.();
|
||||||
|
|
||||||
|
await syncConfig({
|
||||||
|
write: async (config) => {
|
||||||
|
called();
|
||||||
|
// it's an important test, because the `jwt` part is omitted if secrets=false in general app.toJSON()
|
||||||
|
// but it's required to get the app running
|
||||||
|
expect(config.auth.jwt.secret).toBe("");
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
includeSecrets: false,
|
||||||
|
includeFirstBoot: true,
|
||||||
|
})(app).onBuilt?.();
|
||||||
|
expect(called).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { registries } from "../../src";
|
import { registries } from "../../src";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("repros", async () => {
|
describe("repros", async () => {
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +92,7 @@ describe("repros", async () => {
|
|||||||
fns.relation(schema.product_likes).manyToOne(schema.users);
|
fns.relation(schema.product_likes).manyToOne(schema.users);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const app = createApp({ initialConfig: { data: schema.toJSON() } });
|
const app = createApp({ config: { data: schema.toJSON() } });
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||||
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ describe("Core Utils", async () => {
|
|||||||
expect(utils.getContentName(request)).toBe(name);
|
expect(utils.getContentName(request)).toBe(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.only("detectImageDimensions", async () => {
|
test("detectImageDimensions", async () => {
|
||||||
// wrong
|
// wrong
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
|
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
|
||||||
@@ -267,12 +267,12 @@ describe("Core Utils", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("dates", () => {
|
describe("dates", () => {
|
||||||
test.only("formats local time", () => {
|
test("formats local time", () => {
|
||||||
expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
|
expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
|
||||||
console.log(utils.datetimeStringUTC(new Date()));
|
/*console.log(utils.datetimeStringUTC(new Date()));
|
||||||
console.log(utils.datetimeStringUTC());
|
console.log(utils.datetimeStringUTC());
|
||||||
console.log(new Date());
|
console.log(new Date());
|
||||||
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
|
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone); */
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { parse } from "core/utils/schema";
|
|||||||
|
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
||||||
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
||||||
import { Entity, EntityManager, type EntityData } from "data/entities";
|
import { Entity, EntityManager, type EntityData } from "data/entities";
|
||||||
@@ -13,7 +14,7 @@ import { TextField } from "data/fields";
|
|||||||
import { ManyToOneRelation } from "data/relations";
|
import { ManyToOneRelation } from "data/relations";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
beforeAll(() => disableConsoleLog());
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
const dataConfig = parse(dataConfigSchema, {});
|
const dataConfig = parse(dataConfigSchema, {});
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import { ManyToOneRelation } from "data/relations";
|
import { ManyToOneRelation } from "data/relations";
|
||||||
import { TextField } from "data/fields";
|
import { TextField } from "data/fields";
|
||||||
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("[data] JoinBuilder", async () => {
|
describe("[data] JoinBuilder", async () => {
|
||||||
test("missing relation", async () => {
|
test("missing relation", async () => {
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import {
|
|||||||
} from "data/relations";
|
} from "data/relations";
|
||||||
import { NumberField, TextField } from "data/fields";
|
import { NumberField, TextField } from "data/fields";
|
||||||
import * as proto from "data/prototype";
|
import * as proto from "data/prototype";
|
||||||
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
|
import { getDummyConnection } from "../../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
import { MutatorEvents } from "data/events";
|
import { MutatorEvents } from "data/events";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
beforeAll(() => disableConsoleLog());
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("[data] Mutator (base)", async () => {
|
describe("[data] Mutator (base)", async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
import { TextField } from "data/fields";
|
import { TextField } from "data/fields";
|
||||||
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
||||||
@@ -6,11 +6,13 @@ import { Entity, EntityManager } from "data/entities";
|
|||||||
import { ManyToOneRelation } from "data/relations";
|
import { ManyToOneRelation } from "data/relations";
|
||||||
import { RepositoryEvents } from "data/events";
|
import { RepositoryEvents } from "data/events";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
type E = Kysely<any> | Transaction<any>;
|
type E = Kysely<any> | Transaction<any>;
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
async function sleep(ms: number) {
|
async function sleep(ms: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager } from "data/entities";
|
||||||
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
||||||
import { TextField } from "data/fields";
|
import { TextField } from "data/fields";
|
||||||
@@ -6,6 +6,10 @@ import * as proto from "data/prototype";
|
|||||||
import { WithBuilder } from "data/entities/query/WithBuilder";
|
import { WithBuilder } from "data/entities/query/WithBuilder";
|
||||||
import { schemaToEm } from "../../helper";
|
import { schemaToEm } from "../../helper";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,4 @@ describe("FieldIndex", async () => {
|
|||||||
expect(index.name).toEqual("idx_test_name");
|
expect(index.name).toEqual("idx_test_name");
|
||||||
expect(index.unique).toEqual(false);
|
expect(index.unique).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("it fails on non-unique", async () => {
|
|
||||||
const field = new TestField("name", { required: false });
|
|
||||||
|
|
||||||
expect(() => new EntityIndex(entity, [field], true)).toThrowError();
|
|
||||||
expect(() => new EntityIndex(entity, [field])).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import {
|
|||||||
type BaseRelationConfig,
|
type BaseRelationConfig,
|
||||||
EntityRelation,
|
EntityRelation,
|
||||||
EntityRelationAnchor,
|
EntityRelationAnchor,
|
||||||
|
ManyToManyRelation,
|
||||||
RelationTypes,
|
RelationTypes,
|
||||||
} from "data/relations";
|
} from "data/relations";
|
||||||
|
import * as proto from "data/prototype";
|
||||||
|
|
||||||
class TestEntityRelation extends EntityRelation {
|
class TestEntityRelation extends EntityRelation {
|
||||||
constructor(config?: BaseRelationConfig) {
|
constructor(config?: BaseRelationConfig) {
|
||||||
@@ -75,4 +77,15 @@ describe("[data] EntityRelation", async () => {
|
|||||||
const relation2 = new TestEntityRelation({ required: true });
|
const relation2 = new TestEntityRelation({ required: true });
|
||||||
expect(relation2.required).toBe(true);
|
expect(relation2.required).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("correctly produces the relation name", async () => {
|
||||||
|
const relation = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"));
|
||||||
|
expect(relation.getName()).not.toContain(",");
|
||||||
|
expect(relation.getName()).toBe("mn_apps_organizations");
|
||||||
|
|
||||||
|
const relation2 = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"), {
|
||||||
|
connectionTableMappedName: "appOrganizations",
|
||||||
|
});
|
||||||
|
expect(relation2.getName()).toBe("mn_apps_organizations_appOrganizations");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-21
@@ -39,26 +39,6 @@ export function getLocalLibsqlConnection() {
|
|||||||
return { url: "http://127.0.0.1:8080" };
|
return { url: "http://127.0.0.1:8080" };
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConsoleSeverity = "debug" | "log" | "warn" | "error";
|
|
||||||
const _oldConsoles = {
|
|
||||||
debug: console.debug,
|
|
||||||
log: console.log,
|
|
||||||
warn: console.warn,
|
|
||||||
error: console.error,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["debug", "log", "warn"]) {
|
|
||||||
severities.forEach((severity) => {
|
|
||||||
console[severity] = () => null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enableConsoleLog() {
|
|
||||||
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
|
||||||
console[severity as ConsoleSeverity] = fn;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||||
const { sql, parameters } = qb.compile();
|
const { sql, parameters } = qb.compile();
|
||||||
return { sql, parameters };
|
return { sql, parameters };
|
||||||
@@ -66,7 +46,7 @@ export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
|||||||
|
|
||||||
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||||
const { sql, parameters } = qb.compile();
|
const { sql, parameters } = qb.compile();
|
||||||
console.log("$", sqlFormat(sql), "\n[params]", parameters);
|
console.info("$", sqlFormat(sql), "\n[params]", parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { App, createApp } from "../../src";
|
import { App, createApp, type AuthResponse } from "../../src";
|
||||||
import type { AuthResponse } from "../../src/auth";
|
|
||||||
import { auth } from "../../src/auth/middlewares";
|
import { auth } from "../../src/auth/middlewares";
|
||||||
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
||||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
import { getDummyConnection } from "../helper";
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
|
||||||
afterEach(afterAllCleanup);
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -66,9 +63,10 @@ const configs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function createAuthApp() {
|
function createAuthApp() {
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
initialConfig: {
|
config: {
|
||||||
auth: configs.auth,
|
auth: configs.auth,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -151,8 +149,8 @@ describe("integration auth", () => {
|
|||||||
|
|
||||||
const { data: users } = await app.em.repository("users").findMany();
|
const { data: users } = await app.em.repository("users").findMany();
|
||||||
expect(users.length).toBe(2);
|
expect(users.length).toBe(2);
|
||||||
expect(users[0].email).toBe(configs.users.normal.email);
|
expect(users[0]?.email).toBe(configs.users.normal.email);
|
||||||
expect(users[1].email).toBe(configs.users.admin.email);
|
expect(users[1]?.email).toBe(configs.users.admin.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log you in with API", async () => {
|
it("should log you in with API", async () => {
|
||||||
@@ -223,7 +221,7 @@ describe("integration auth", () => {
|
|||||||
|
|
||||||
app.server.get("/get", auth(), async (c) => {
|
app.server.get("/get", auth(), async (c) => {
|
||||||
return c.json({
|
return c.json({
|
||||||
user: c.get("auth").user ?? null,
|
user: c.get("auth")?.user ?? null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.server.get("/wait", auth(), async (c) => {
|
app.server.get("/wait", auth(), async (c) => {
|
||||||
@@ -242,7 +240,7 @@ describe("integration auth", () => {
|
|||||||
{
|
{
|
||||||
await new Promise((r) => setTimeout(r, 10));
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
const res = await app.server.request("/get");
|
const res = await app.server.request("/get");
|
||||||
const data = await res.json();
|
const data = (await res.json()) as any;
|
||||||
expect(data.user).toBe(null);
|
expect(data.user).toBe(null);
|
||||||
expect(await $fns.me()).toEqual({ user: null as any });
|
expect(await $fns.me()).toEqual({ user: null as any });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import { Api } from "../../src/Api";
|
import { Api } from "../../src/Api";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("integration config", () => {
|
describe("integration config", () => {
|
||||||
it("should create an entity", async () => {
|
it("should create an entity", async () => {
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ import { createApp } from "core/test/utils";
|
|||||||
import { mergeObject, randomString } from "../../src/core/utils";
|
import { mergeObject, randomString } from "../../src/core/utils";
|
||||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
|
import { assetsPath, assetsTmpPath } from "../helper";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
disableConsoleLog();
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
});
|
});
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
const path = `${assetsPath}/image.png`;
|
const path = `${assetsPath}/image.png`;
|
||||||
|
|
||||||
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
media: mergeObject(
|
media: mergeObject(
|
||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -40,9 +43,6 @@ function makeName(ext: string) {
|
|||||||
return randomString(10) + "." + ext;
|
return randomString(10) + "." + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("MediaController", () => {
|
describe("MediaController", () => {
|
||||||
test("accepts direct", async () => {
|
test("accepts direct", async () => {
|
||||||
const app = await makeApp();
|
const app = await makeApp();
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { createApp } from "core/test/utils";
|
|||||||
import { AuthController } from "../../src/auth/api/AuthController";
|
import { AuthController } from "../../src/auth/api/AuthController";
|
||||||
import { em, entity, make, text } from "data/prototype";
|
import { em, entity, make, text } from "data/prototype";
|
||||||
import { AppAuth, type ModuleBuildContext } from "modules";
|
import { AppAuth, type ModuleBuildContext } from "modules";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("AppAuth", () => {
|
describe("AppAuth", () => {
|
||||||
test.only("...", () => {
|
test.skip("...", () => {
|
||||||
const auth = new AppAuth({});
|
const auth = new AppAuth({});
|
||||||
console.log(auth.toJSON());
|
console.log(auth.toJSON());
|
||||||
console.log(auth.config);
|
console.log(auth.config);
|
||||||
@@ -147,7 +150,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("registers auth middleware for bknd routes only", async () => {
|
test("registers auth middleware for bknd routes only", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
jwt: {
|
jwt: {
|
||||||
@@ -177,7 +180,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("should allow additional user fields", async () => {
|
test("should allow additional user fields", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
entity_name: "users",
|
entity_name: "users",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -201,7 +204,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("ensure user field configs is always correct", async () => {
|
test("ensure user field configs is always correct", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AppMedia } from "../../src/media/AppMedia";
|
|||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
|
|
||||||
describe("AppMedia", () => {
|
describe("AppMedia", () => {
|
||||||
test.only("...", () => {
|
test.skip("...", () => {
|
||||||
const media = new AppMedia();
|
const media = new AppMedia();
|
||||||
console.log(media.toJSON());
|
console.log(media.toJSON());
|
||||||
});
|
});
|
||||||
@@ -18,7 +18,7 @@ describe("AppMedia", () => {
|
|||||||
registries.media.register("local", StorageLocalAdapter);
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
media: {
|
media: {
|
||||||
entity_name: "media",
|
entity_name: "media",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { it, expect, describe } from "bun:test";
|
||||||
|
import { DbModuleManager } from "modules/db/DbModuleManager";
|
||||||
|
import { getDummyConnection } from "../helper";
|
||||||
|
import { TABLE_NAME } from "modules/db/migrations";
|
||||||
|
|
||||||
|
describe("DbModuleManager", () => {
|
||||||
|
it("should extract secrets", async () => {
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
const m = new DbModuleManager(dummyConnection, {
|
||||||
|
initial: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await m.build();
|
||||||
|
expect(m.toJSON(true).auth.jwt.secret).toBe("test");
|
||||||
|
await m.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with initial secrets", async () => {
|
||||||
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
const db = dummyConnection.kysely;
|
||||||
|
const m = new DbModuleManager(dummyConnection, {
|
||||||
|
initial: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
"auth.jwt.secret": "test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await m.build();
|
||||||
|
expect(m.toJSON(true).auth.jwt.secret).toBe("test");
|
||||||
|
|
||||||
|
const getSecrets = () =>
|
||||||
|
db
|
||||||
|
.selectFrom(TABLE_NAME)
|
||||||
|
.selectAll()
|
||||||
|
.where("type", "=", "secrets")
|
||||||
|
.executeTakeFirst()
|
||||||
|
.then((r) => r?.json);
|
||||||
|
|
||||||
|
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
|
||||||
|
|
||||||
|
// also after rebuild
|
||||||
|
await m.build();
|
||||||
|
await m.save();
|
||||||
|
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
|
||||||
|
|
||||||
|
// and ignore if already present
|
||||||
|
const m2 = new DbModuleManager(dummyConnection, {
|
||||||
|
initial: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
"auth.jwt.secret": "something completely different",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await m2.build();
|
||||||
|
await m2.save();
|
||||||
|
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
import { getDefaultConfig } from "modules/ModuleManager";
|
||||||
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
import { type ConfigTable, DbModuleManager as ModuleManager } from "modules/db/DbModuleManager";
|
||||||
|
|
||||||
|
import { CURRENT_VERSION, TABLE_NAME } from "modules/db/migrations";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { s, stripMark } from "core/utils/schema";
|
import { s, stripMark } from "core/utils/schema";
|
||||||
import { Connection } from "data/connection/Connection";
|
import { Connection } from "data/connection/Connection";
|
||||||
import { entity, text } from "data/prototype";
|
import { entity, text } from "data/prototype";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("ModuleManager", async () => {
|
describe("ModuleManager", async () => {
|
||||||
test("s1: no config, no build", async () => {
|
test("s1: no config, no build", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -133,7 +138,7 @@ describe("ModuleManager", async () => {
|
|||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
|
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||||
initial: { version: version - 1, ...json },
|
initial: { version: version - 1, ...json } as any,
|
||||||
});
|
});
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
await db
|
await db
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { type InitialModuleConfigs, createApp } from "../../../src";
|
import { type InitialModuleConfigs, createApp } from "../../../src";
|
||||||
|
|
||||||
import { type Kysely, sql } from "kysely";
|
import { type Kysely, sql } from "kysely";
|
||||||
@@ -6,6 +6,10 @@ import { getDummyConnection } from "../../helper";
|
|||||||
import v7 from "./samples/v7.json";
|
import v7 from "./samples/v7.json";
|
||||||
import v8 from "./samples/v8.json";
|
import v8 from "./samples/v8.json";
|
||||||
import v8_2 from "./samples/v8-2.json";
|
import v8_2 from "./samples/v8-2.json";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
// app expects migratable config to be present in database
|
// app expects migratable config to be present in database
|
||||||
async function createVersionedApp(config: InitialModuleConfigs | any) {
|
async function createVersionedApp(config: InitialModuleConfigs | any) {
|
||||||
|
|||||||
+9
-4
@@ -3,20 +3,25 @@ import c from "picocolors";
|
|||||||
import { formatNumber } from "bknd/utils";
|
import { formatNumber } from "bknd/utils";
|
||||||
import * as esbuild from "esbuild";
|
import * as esbuild from "esbuild";
|
||||||
|
|
||||||
|
const deps = Object.keys(pkg.dependencies);
|
||||||
|
const external = ["jsonv-ts/*", "wrangler", ...deps];
|
||||||
|
|
||||||
if (process.env.DEBUG) {
|
if (process.env.DEBUG) {
|
||||||
await esbuild.build({
|
const result = await esbuild.build({
|
||||||
entryPoints: ["./src/cli/index.ts"],
|
entryPoints: ["./src/cli/index.ts"],
|
||||||
outdir: "./dist/cli",
|
outdir: "./dist/cli",
|
||||||
platform: "node",
|
platform: "node",
|
||||||
minify: false,
|
minify: true,
|
||||||
format: "esm",
|
format: "esm",
|
||||||
|
metafile: true,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
external: ["jsonv-ts", "jsonv-ts/*"],
|
external,
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
__version: JSON.stringify(pkg.version),
|
__version: JSON.stringify(pkg.version),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await Bun.write("./dist/cli/metafile-esm.json", JSON.stringify(result.metafile, null, 2));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +31,7 @@ const result = await Bun.build({
|
|||||||
outdir: "./dist/cli",
|
outdir: "./dist/cli",
|
||||||
env: "PUBLIC_*",
|
env: "PUBLIC_*",
|
||||||
minify: true,
|
minify: true,
|
||||||
external: ["jsonv-ts", "jsonv-ts/*"],
|
external,
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
__version: JSON.stringify(pkg.version),
|
__version: JSON.stringify(pkg.version),
|
||||||
|
|||||||
@@ -252,6 +252,8 @@ async function buildAdapters() {
|
|||||||
platform: "neutral",
|
platform: "neutral",
|
||||||
entry: ["src/adapter/index.ts"],
|
entry: ["src/adapter/index.ts"],
|
||||||
outDir: "dist/adapter",
|
outDir: "dist/adapter",
|
||||||
|
// only way to keep @vite-ignore comments
|
||||||
|
minify: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// specific adatpers
|
// specific adatpers
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createApp } from "bknd/adapter/bun";
|
|||||||
async function generate() {
|
async function generate() {
|
||||||
console.info("Generating MCP documentation...");
|
console.info("Generating MCP documentation...");
|
||||||
const app = await createApp({
|
const app = await createApp({
|
||||||
initialConfig: {
|
config: {
|
||||||
server: {
|
server: {
|
||||||
mcp: {
|
mcp: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
+7
-5
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.17.2",
|
"version": "0.18.0-rc.4",
|
||||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||||
"homepage": "https://bknd.io",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
||||||
"updater": "bun x npm-check-updates -ui",
|
"updater": "bun x npm-check-updates -ui",
|
||||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||||
"prepublishOnly": "bun run types && bun run test && bun run test:node && VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./",
|
"prepublishOnly": "bun run types && bun run test && bun run test:node && NODE_NO_WARNINGS=1 VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./",
|
||||||
"postpublish": "rm -f README.md",
|
"postpublish": "rm -f README.md",
|
||||||
"test": "ALL_TESTS=1 bun test --bail",
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
"test:all": "bun run test && bun run test:node",
|
"test:all": "bun run test && bun run test:node",
|
||||||
@@ -78,10 +78,10 @@
|
|||||||
"@aws-sdk/client-s3": "^3.758.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
"@bluwy/giget-core": "^0.1.2",
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
"@clack/prompts": "^0.11.0",
|
"@clack/prompts": "^0.11.0",
|
||||||
"@cloudflare/vitest-pool-workers": "^0.8.38",
|
"@cloudflare/vitest-pool-workers": "^0.9.3",
|
||||||
"@cloudflare/workers-types": "^4.20250606.0",
|
"@cloudflare/workers-types": "^4.20250606.0",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hono/vite-dev-server": "^0.19.1",
|
"@hono/vite-dev-server": "^0.21.0",
|
||||||
"@hookform/resolvers": "^4.1.3",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@libsql/client": "^0.15.9",
|
"@libsql/client": "^0.15.9",
|
||||||
"@mantine/modals": "^7.17.1",
|
"@mantine/modals": "^7.17.1",
|
||||||
@@ -130,7 +130,9 @@
|
|||||||
"vite-plugin-circular-dependency": "^0.5.0",
|
"vite-plugin-circular-dependency": "^0.5.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.0.9",
|
"vitest": "^3.0.9",
|
||||||
"wouter": "^3.6.0"
|
"wouter": "^3.6.0",
|
||||||
|
"wrangler": "^4.37.1",
|
||||||
|
"miniflare": "^4.20250913.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@hono/node-server": "^1.14.3"
|
"@hono/node-server": "^1.14.3"
|
||||||
|
|||||||
+33
-21
@@ -5,17 +5,18 @@ import type { em as prototypeEm } from "data/prototype";
|
|||||||
import { Connection } from "data/connection/Connection";
|
import { Connection } from "data/connection/Connection";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
ModuleManager,
|
|
||||||
type InitialModuleConfigs,
|
type InitialModuleConfigs,
|
||||||
type ModuleBuildContext,
|
|
||||||
type ModuleConfigs,
|
type ModuleConfigs,
|
||||||
type ModuleManagerOptions,
|
|
||||||
type Modules,
|
type Modules,
|
||||||
|
ModuleManager,
|
||||||
|
type ModuleBuildContext,
|
||||||
|
type ModuleManagerOptions,
|
||||||
} from "modules/ModuleManager";
|
} from "modules/ModuleManager";
|
||||||
|
import { DbModuleManager } from "modules/db/DbModuleManager";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import { SystemController } from "modules/server/SystemController";
|
import { SystemController } from "modules/server/SystemController";
|
||||||
import type { MaybePromise } from "core/types";
|
import type { MaybePromise, PartialRec } from "core/types";
|
||||||
import type { ServerEnv } from "modules/Controller";
|
import type { ServerEnv } from "modules/Controller";
|
||||||
import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
||||||
|
|
||||||
@@ -93,20 +94,23 @@ export type AppOptions = {
|
|||||||
email?: IEmailDriver;
|
email?: IEmailDriver;
|
||||||
cache?: ICacheDriver;
|
cache?: ICacheDriver;
|
||||||
};
|
};
|
||||||
|
mode?: "db" | "code";
|
||||||
|
readonly?: boolean;
|
||||||
};
|
};
|
||||||
export type CreateAppConfig = {
|
export type CreateAppConfig = {
|
||||||
/**
|
|
||||||
* bla
|
|
||||||
*/
|
|
||||||
connection?: Connection | { url: string };
|
connection?: Connection | { url: string };
|
||||||
initialConfig?: InitialModuleConfigs;
|
config?: PartialRec<ModuleConfigs>;
|
||||||
options?: AppOptions;
|
options?: AppOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppConfig = InitialModuleConfigs;
|
export type AppConfig = { version: number } & ModuleConfigs;
|
||||||
export type LocalApiOptions = Request | ApiOptions;
|
export type LocalApiOptions = Request | ApiOptions;
|
||||||
|
|
||||||
export class App<C extends Connection = Connection, Options extends AppOptions = AppOptions> {
|
export class App<
|
||||||
|
C extends Connection = Connection,
|
||||||
|
Config extends PartialRec<ModuleConfigs> = PartialRec<ModuleConfigs>,
|
||||||
|
Options extends AppOptions = AppOptions,
|
||||||
|
> {
|
||||||
static readonly Events = AppEvents;
|
static readonly Events = AppEvents;
|
||||||
|
|
||||||
modules: ModuleManager;
|
modules: ModuleManager;
|
||||||
@@ -121,8 +125,8 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public connection: C,
|
public connection: C,
|
||||||
_initialConfig?: InitialModuleConfigs,
|
_config?: Config,
|
||||||
private options?: Options,
|
public options?: Options,
|
||||||
) {
|
) {
|
||||||
this.drivers = options?.drivers ?? {};
|
this.drivers = options?.drivers ?? {};
|
||||||
|
|
||||||
@@ -134,9 +138,13 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
|
|||||||
this.plugins.set(config.name, config);
|
this.plugins.set(config.name, config);
|
||||||
}
|
}
|
||||||
this.runPlugins("onBoot");
|
this.runPlugins("onBoot");
|
||||||
this.modules = new ModuleManager(connection, {
|
|
||||||
|
// use db manager by default
|
||||||
|
const Manager = this.mode === "db" ? DbModuleManager : ModuleManager;
|
||||||
|
|
||||||
|
this.modules = new Manager(connection, {
|
||||||
...(options?.manager ?? {}),
|
...(options?.manager ?? {}),
|
||||||
initial: _initialConfig,
|
initial: _config,
|
||||||
onUpdated: this.onUpdated.bind(this),
|
onUpdated: this.onUpdated.bind(this),
|
||||||
onFirstBoot: this.onFirstBoot.bind(this),
|
onFirstBoot: this.onFirstBoot.bind(this),
|
||||||
onServerInit: this.onServerInit.bind(this),
|
onServerInit: this.onServerInit.bind(this),
|
||||||
@@ -145,6 +153,14 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
|
|||||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.options?.mode ?? "db";
|
||||||
|
}
|
||||||
|
|
||||||
|
isReadOnly() {
|
||||||
|
return Boolean(this.mode === "code" || this.options?.readonly);
|
||||||
|
}
|
||||||
|
|
||||||
get emgr() {
|
get emgr() {
|
||||||
return this.modules.ctx().emgr;
|
return this.modules.ctx().emgr;
|
||||||
}
|
}
|
||||||
@@ -175,7 +191,7 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
|
|||||||
return results as any;
|
return results as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
async build(options?: { sync?: boolean; forceBuild?: boolean; [key: string]: any }) {
|
||||||
// prevent multiple concurrent builds
|
// prevent multiple concurrent builds
|
||||||
if (this._building) {
|
if (this._building) {
|
||||||
while (this._building) {
|
while (this._building) {
|
||||||
@@ -188,7 +204,7 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
|
|||||||
this._building = true;
|
this._building = true;
|
||||||
|
|
||||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||||
await this.modules.build({ fetch: options?.fetch });
|
await this.modules.build();
|
||||||
|
|
||||||
const { guard } = this.modules.ctx();
|
const { guard } = this.modules.ctx();
|
||||||
|
|
||||||
@@ -215,10 +231,6 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
|
|||||||
this._building = false;
|
this._building = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateConfig<Module extends keyof Modules>(module: Module) {
|
|
||||||
return this.modules.mutateConfigSafe(module);
|
|
||||||
}
|
|
||||||
|
|
||||||
get server() {
|
get server() {
|
||||||
return this.modules.server;
|
return this.modules.server;
|
||||||
}
|
}
|
||||||
@@ -377,5 +389,5 @@ export function createApp(config: CreateAppConfig = {}) {
|
|||||||
throw new Error("Invalid connection");
|
throw new Error("Invalid connection");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new App(config.connection, config.initialConfig, config.options);
|
return new App(config.connection, config.config, config.options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { TestRunner } from "core/test";
|
import type { TestRunner } from "core/test";
|
||||||
import type { BkndConfig, DefaultArgs, FrameworkOptions, RuntimeOptions } from "./index";
|
import type { BkndConfig, DefaultArgs } from "./index";
|
||||||
import type { App } from "App";
|
import type { App } from "App";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
export function adapterTestSuite<
|
export function adapterTestSuite<
|
||||||
Config extends BkndConfig = BkndConfig,
|
Config extends BkndConfig = BkndConfig,
|
||||||
@@ -13,24 +14,17 @@ export function adapterTestSuite<
|
|||||||
label = "app",
|
label = "app",
|
||||||
overrides = {},
|
overrides = {},
|
||||||
}: {
|
}: {
|
||||||
makeApp: (
|
makeApp: (config: Config, args?: Args) => Promise<App>;
|
||||||
config: Config,
|
makeHandler?: (config?: Config, args?: Args) => (request: Request) => Promise<Response>;
|
||||||
args?: Args,
|
|
||||||
opts?: RuntimeOptions | FrameworkOptions,
|
|
||||||
) => Promise<App>;
|
|
||||||
makeHandler?: (
|
|
||||||
config?: Config,
|
|
||||||
args?: Args,
|
|
||||||
opts?: RuntimeOptions | FrameworkOptions,
|
|
||||||
) => (request: Request) => Promise<Response>;
|
|
||||||
label?: string;
|
label?: string;
|
||||||
overrides?: {
|
overrides?: {
|
||||||
dbUrl?: string;
|
dbUrl?: string;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { test, expect, mock } = testRunner;
|
const { test, expect, mock, beforeAll, afterAll } = testRunner;
|
||||||
const id = crypto.randomUUID();
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(() => enableConsoleLog());
|
||||||
|
|
||||||
test(`creates ${label}`, async () => {
|
test(`creates ${label}`, async () => {
|
||||||
const beforeBuild = mock(async () => null) as any;
|
const beforeBuild = mock(async () => null) as any;
|
||||||
@@ -39,7 +33,7 @@ export function adapterTestSuite<
|
|||||||
const config = {
|
const config = {
|
||||||
app: (env) => ({
|
app: (env) => ({
|
||||||
connection: { url: env.url },
|
connection: { url: env.url },
|
||||||
initialConfig: {
|
config: {
|
||||||
server: { cors: { origin: env.origin } },
|
server: { cors: { origin: env.origin } },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -53,11 +47,10 @@ export function adapterTestSuite<
|
|||||||
url: overrides.dbUrl ?? ":memory:",
|
url: overrides.dbUrl ?? ":memory:",
|
||||||
origin: "localhost",
|
origin: "localhost",
|
||||||
} as any,
|
} as any,
|
||||||
{ force: false, id },
|
|
||||||
);
|
);
|
||||||
expect(app).toBeDefined();
|
expect(app).toBeDefined();
|
||||||
expect(app.toJSON().server.cors.origin).toEqual("localhost");
|
expect(app.toJSON().server.cors.origin).toEqual("localhost");
|
||||||
expect(beforeBuild).toHaveBeenCalledTimes(1);
|
expect(beforeBuild).toHaveBeenCalledTimes(2);
|
||||||
expect(onBuilt).toHaveBeenCalledTimes(1);
|
expect(onBuilt).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,8 +61,8 @@ export function adapterTestSuite<
|
|||||||
return { res, data };
|
return { res, data };
|
||||||
};
|
};
|
||||||
|
|
||||||
test("responds with the same app id", async () => {
|
/* test.skip("responds with the same app id", async () => {
|
||||||
const fetcher = makeHandler(undefined, undefined, { force: false, id });
|
const fetcher = makeHandler(undefined, undefined, { id });
|
||||||
|
|
||||||
const { res, data } = await getConfig(fetcher);
|
const { res, data } = await getConfig(fetcher);
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
@@ -77,14 +70,14 @@ export function adapterTestSuite<
|
|||||||
expect(data.server.cors.origin).toEqual("localhost");
|
expect(data.server.cors.origin).toEqual("localhost");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("creates fresh & responds to api config", async () => {
|
test.skip("creates fresh & responds to api config", async () => {
|
||||||
// set the same id, but force recreate
|
// set the same id, but force recreate
|
||||||
const fetcher = makeHandler(undefined, undefined, { id, force: true });
|
const fetcher = makeHandler(undefined, undefined, { id });
|
||||||
|
|
||||||
const { res, data } = await getConfig(fetcher);
|
const { res, data } = await getConfig(fetcher);
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(data.server.cors.origin).toEqual("*");
|
expect(data.server.cors.origin).toEqual("*");
|
||||||
});
|
}); */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ afterAll(enableConsoleLog);
|
|||||||
describe("astro adapter", () => {
|
describe("astro adapter", () => {
|
||||||
adapterTestSuite(bunTestRunner, {
|
adapterTestSuite(bunTestRunner, {
|
||||||
makeApp: astro.getApp,
|
makeApp: astro.getApp,
|
||||||
makeHandler: (c, a, o) => (request: Request) => astro.serve(c, a, o)({ request }),
|
makeHandler: (c, a) => (request: Request) => astro.serve(c, a)({ request }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FrameworkBkndConfig, createFrameworkApp, type FrameworkOptions } from "bknd/adapter";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||||
|
|
||||||
type AstroEnv = NodeJS.ProcessEnv;
|
type AstroEnv = NodeJS.ProcessEnv;
|
||||||
type TAstro = {
|
type TAstro = {
|
||||||
@@ -9,17 +9,12 @@ export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
|
|||||||
export async function getApp<Env = AstroEnv>(
|
export async function getApp<Env = AstroEnv>(
|
||||||
config: AstroBkndConfig<Env> = {},
|
config: AstroBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts: FrameworkOptions = {},
|
|
||||||
) {
|
) {
|
||||||
return await createFrameworkApp(config, args ?? import.meta.env, opts);
|
return await createFrameworkApp(config, args ?? import.meta.env);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<Env = AstroEnv>(
|
export function serve<Env = AstroEnv>(config: AstroBkndConfig<Env> = {}, args: Env = {} as Env) {
|
||||||
config: AstroBkndConfig<Env> = {},
|
|
||||||
args: Env = {} as Env,
|
|
||||||
opts?: FrameworkOptions,
|
|
||||||
) {
|
|
||||||
return async (fnArgs: TAstro) => {
|
return async (fnArgs: TAstro) => {
|
||||||
return (await getApp(config, args, opts)).fetch(fnArgs.request);
|
return (await getApp(config, args)).fetch(fnArgs.request);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { handle } from "hono/aws-lambda";
|
import { handle } from "hono/aws-lambda";
|
||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||||
|
|
||||||
type AwsLambdaEnv = object;
|
type AwsLambdaEnv = object;
|
||||||
export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
|
export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
|
||||||
@@ -20,7 +20,6 @@ export type AwsLambdaBkndConfig<Env extends AwsLambdaEnv = AwsLambdaEnv> =
|
|||||||
export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||||
{ adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {},
|
{ adminOptions = false, assets, ...config }: AwsLambdaBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
let additional: Partial<RuntimeBkndConfig> = {
|
let additional: Partial<RuntimeBkndConfig> = {
|
||||||
adminOptions,
|
adminOptions,
|
||||||
@@ -57,17 +56,15 @@ export async function createApp<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
|||||||
...additional,
|
...additional,
|
||||||
},
|
},
|
||||||
args ?? process.env,
|
args ?? process.env,
|
||||||
opts,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
export function serve<Env extends AwsLambdaEnv = AwsLambdaEnv>(
|
||||||
config: AwsLambdaBkndConfig<Env> = {},
|
config: AwsLambdaBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
|
||||||
) {
|
) {
|
||||||
return async (event) => {
|
return async (event) => {
|
||||||
const app = await createApp(config, args, opts);
|
const app = await createApp(config, args);
|
||||||
return await handle(app.server)(event);
|
return await handle(app.server)(event);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ describe("aws adapter", () => {
|
|||||||
adapterTestSuite(bunTestRunner, {
|
adapterTestSuite(bunTestRunner, {
|
||||||
makeApp: awsLambda.createApp,
|
makeApp: awsLambda.createApp,
|
||||||
// @todo: add a request to lambda event translator?
|
// @todo: add a request to lambda event translator?
|
||||||
makeHandler: (c, a, o) => async (request: Request) => {
|
makeHandler: (c, a) => async (request: Request) => {
|
||||||
const app = await awsLambda.createApp(c, a, o);
|
const app = await awsLambda.createApp(c, a);
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// <reference types="bun-types" />
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||||
import { registerLocalMediaAdapter } from ".";
|
import { registerLocalMediaAdapter } from ".";
|
||||||
import { config, type App } from "bknd";
|
import { config, type App } from "bknd";
|
||||||
import type { ServeOptions } from "bun";
|
import type { ServeOptions } from "bun";
|
||||||
@@ -13,7 +13,6 @@ export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOpt
|
|||||||
export async function createApp<Env = BunEnv>(
|
export async function createApp<Env = BunEnv>(
|
||||||
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
|
{ distPath, serveStatic: _serveStatic, ...config }: BunBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
|
||||||
) {
|
) {
|
||||||
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||||
registerLocalMediaAdapter();
|
registerLocalMediaAdapter();
|
||||||
@@ -28,19 +27,17 @@ export async function createApp<Env = BunEnv>(
|
|||||||
...config,
|
...config,
|
||||||
},
|
},
|
||||||
args ?? (process.env as Env),
|
args ?? (process.env as Env),
|
||||||
opts,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHandler<Env = BunEnv>(
|
export function createHandler<Env = BunEnv>(
|
||||||
config: BunBkndConfig<Env> = {},
|
config: BunBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
|
||||||
) {
|
) {
|
||||||
let app: App | undefined;
|
let app: App | undefined;
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = await createApp(config, args ?? (process.env as Env), opts);
|
app = await createApp(config, args ?? (process.env as Env));
|
||||||
}
|
}
|
||||||
return app.fetch(req);
|
return app.fetch(req);
|
||||||
};
|
};
|
||||||
@@ -50,7 +47,7 @@ export function serve<Env = BunEnv>(
|
|||||||
{
|
{
|
||||||
distPath,
|
distPath,
|
||||||
connection,
|
connection,
|
||||||
initialConfig,
|
config: _config,
|
||||||
options,
|
options,
|
||||||
port = config.server.default_port,
|
port = config.server.default_port,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
@@ -60,7 +57,6 @@ export function serve<Env = BunEnv>(
|
|||||||
...serveOptions
|
...serveOptions
|
||||||
}: BunBkndConfig<Env> = {},
|
}: BunBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
|
||||||
) {
|
) {
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
...serveOptions,
|
...serveOptions,
|
||||||
@@ -68,7 +64,7 @@ export function serve<Env = BunEnv>(
|
|||||||
fetch: createHandler(
|
fetch: createHandler(
|
||||||
{
|
{
|
||||||
connection,
|
connection,
|
||||||
initialConfig,
|
config: _config,
|
||||||
options,
|
options,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
buildConfig,
|
buildConfig,
|
||||||
@@ -77,7 +73,6 @@ export function serve<Env = BunEnv>(
|
|||||||
serveStatic,
|
serveStatic,
|
||||||
},
|
},
|
||||||
args,
|
args,
|
||||||
opts,
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
import { bunSqlite } from "./BunSqliteConnection";
|
import { bunSqlite } from "./BunSqliteConnection";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { describe } from "bun:test";
|
import { describe, test, mock, expect } from "bun:test";
|
||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
|
import { GenericSqliteConnection } from "data/connection/sqlite/GenericSqliteConnection";
|
||||||
|
|
||||||
describe("BunSqliteConnection", () => {
|
describe("BunSqliteConnection", () => {
|
||||||
connectionTestSuite(bunTestRunner, {
|
connectionTestSuite(bunTestRunner, {
|
||||||
@@ -12,4 +13,20 @@ describe("BunSqliteConnection", () => {
|
|||||||
}),
|
}),
|
||||||
rawDialectDetails: [],
|
rawDialectDetails: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("onCreateConnection", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
|
||||||
|
const conn = bunSqlite({
|
||||||
|
onCreateConnection: (db) => {
|
||||||
|
expect(db).toBeInstanceOf(Database);
|
||||||
|
called();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await conn.ping();
|
||||||
|
|
||||||
|
expect(conn).toBeInstanceOf(GenericSqliteConnection);
|
||||||
|
expect(conn.db).toBeInstanceOf(Database);
|
||||||
|
expect(called).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,40 +1,53 @@
|
|||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
import {
|
||||||
|
genericSqlite,
|
||||||
|
type GenericSqliteConnection,
|
||||||
|
type GenericSqliteConnectionConfig,
|
||||||
|
} from "bknd";
|
||||||
|
import { omitKeys } from "bknd/utils";
|
||||||
|
|
||||||
export type BunSqliteConnection = GenericSqliteConnection<Database>;
|
export type BunSqliteConnection = GenericSqliteConnection<Database>;
|
||||||
export type BunSqliteConnectionConfig = {
|
export type BunSqliteConnectionConfig = Omit<
|
||||||
database: Database;
|
GenericSqliteConnectionConfig<Database>,
|
||||||
};
|
"name" | "supports"
|
||||||
|
> &
|
||||||
|
({ database?: Database; url?: never } | { url?: string; database?: never });
|
||||||
|
|
||||||
export function bunSqlite(config?: BunSqliteConnectionConfig | { url: string }) {
|
export function bunSqlite(config?: BunSqliteConnectionConfig) {
|
||||||
let db: Database;
|
let db: Database | undefined;
|
||||||
if (config) {
|
if (config) {
|
||||||
if ("database" in config) {
|
if ("database" in config && config.database) {
|
||||||
db = config.database;
|
db = config.database;
|
||||||
} else {
|
} else if (config.url) {
|
||||||
db = new Database(config.url);
|
db = new Database(config.url);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
db = new Database(":memory:");
|
db = new Database(":memory:");
|
||||||
}
|
}
|
||||||
|
|
||||||
return genericSqlite("bun-sqlite", db, (utils) => {
|
return genericSqlite(
|
||||||
//const fn = cache ? "query" : "prepare";
|
"bun-sqlite",
|
||||||
const getStmt = (sql: string) => db.prepare(sql);
|
db,
|
||||||
|
(utils) => {
|
||||||
|
const getStmt = (sql: string) => db.prepare(sql);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
db,
|
db,
|
||||||
query: utils.buildQueryFn({
|
query: utils.buildQueryFn({
|
||||||
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
|
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
|
||||||
run: (sql, parameters) => {
|
run: (sql, parameters) => {
|
||||||
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
|
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
|
||||||
return {
|
return {
|
||||||
insertId: utils.parseBigInt(lastInsertRowid),
|
insertId: utils.parseBigInt(lastInsertRowid),
|
||||||
numAffectedRows: utils.parseBigInt(changes),
|
numAffectedRows: utils.parseBigInt(changes),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
close: () => db.close(),
|
close: () => db.close(),
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
omitKeys(config ?? ({} as any), ["database", "url", "name", "supports"]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test, mock, describe, beforeEach, afterEach, afterAll } from "bun:test";
|
import { expect, test, mock, describe, beforeEach, afterEach, afterAll, beforeAll } from "bun:test";
|
||||||
|
|
||||||
export const bunTestRunner = {
|
export const bunTestRunner = {
|
||||||
describe,
|
describe,
|
||||||
@@ -8,4 +8,5 @@ export const bunTestRunner = {
|
|||||||
beforeEach,
|
beforeEach,
|
||||||
afterEach,
|
afterEach,
|
||||||
afterAll,
|
afterAll,
|
||||||
|
beforeAll,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { adapterTestSuite } from "adapter/adapter-test-suite";
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter";
|
import { type CloudflareBkndConfig, createApp } from "./cloudflare-workers.adapter";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
/* beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog); */
|
||||||
|
|
||||||
describe("cf adapter", () => {
|
describe("cf adapter", () => {
|
||||||
const DB_URL = ":memory:";
|
const DB_URL = ":memory:";
|
||||||
@@ -20,31 +20,31 @@ describe("cf adapter", () => {
|
|||||||
const staticConfig = await makeConfig(
|
const staticConfig = await makeConfig(
|
||||||
{
|
{
|
||||||
connection: { url: DB_URL },
|
connection: { url: DB_URL },
|
||||||
initialConfig: { data: { basepath: DB_URL } },
|
config: { data: { basepath: DB_URL } },
|
||||||
},
|
},
|
||||||
$ctx({ DB_URL }),
|
$ctx({ DB_URL }),
|
||||||
);
|
);
|
||||||
expect(staticConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
|
expect(staticConfig.config).toEqual({ data: { basepath: DB_URL } });
|
||||||
expect(staticConfig.connection).toBeDefined();
|
expect(staticConfig.connection).toBeDefined();
|
||||||
|
|
||||||
const dynamicConfig = await makeConfig(
|
const dynamicConfig = await makeConfig(
|
||||||
{
|
{
|
||||||
app: (env) => ({
|
app: (env) => ({
|
||||||
initialConfig: { data: { basepath: env.DB_URL } },
|
config: { data: { basepath: env.DB_URL } },
|
||||||
connection: { url: env.DB_URL },
|
connection: { url: env.DB_URL },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
$ctx({ DB_URL }),
|
$ctx({ DB_URL }),
|
||||||
);
|
);
|
||||||
expect(dynamicConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
|
expect(dynamicConfig.config).toEqual({ data: { basepath: DB_URL } });
|
||||||
expect(dynamicConfig.connection).toBeDefined();
|
expect(dynamicConfig.connection).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, {
|
adapterTestSuite<CloudflareBkndConfig, CloudflareContext<any>>(bunTestRunner, {
|
||||||
makeApp: async (c, a, o) => {
|
makeApp: async (c, a) => {
|
||||||
return await createApp(c, { env: a } as any, o);
|
return await createApp(c, { env: a } as any);
|
||||||
},
|
},
|
||||||
makeHandler: (c, a, o) => {
|
makeHandler: (c, a) => {
|
||||||
console.log("args", a);
|
console.log("args", a);
|
||||||
return async (request: any) => {
|
return async (request: any) => {
|
||||||
const app = await createApp(
|
const app = await createApp(
|
||||||
@@ -53,7 +53,6 @@ describe("cf adapter", () => {
|
|||||||
connection: { url: DB_URL },
|
connection: { url: DB_URL },
|
||||||
},
|
},
|
||||||
a as any,
|
a as any,
|
||||||
o,
|
|
||||||
);
|
);
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Hono } from "hono";
|
|||||||
import { serveStatic } from "hono/cloudflare-workers";
|
import { serveStatic } from "hono/cloudflare-workers";
|
||||||
import type { MaybePromise } from "bknd";
|
import type { MaybePromise } from "bknd";
|
||||||
import { $console } from "bknd/utils";
|
import { $console } from "bknd/utils";
|
||||||
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
import { createRuntimeApp } from "bknd/adapter";
|
||||||
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
|
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -34,12 +34,8 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
|
export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
|
||||||
config: CloudflareBkndConfig<Env>,
|
config: CloudflareBkndConfig<Env> = {},
|
||||||
ctx: Partial<CloudflareContext<Env>> = {},
|
ctx: Partial<CloudflareContext<Env>> = {},
|
||||||
opts: RuntimeOptions = {
|
|
||||||
// by default, require the app to be rebuilt every time
|
|
||||||
force: true,
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
const appConfig = await makeConfig(
|
const appConfig = await makeConfig(
|
||||||
{
|
{
|
||||||
@@ -53,7 +49,7 @@ export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
|
|||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
return await createRuntimeApp<Env>(appConfig, ctx?.env, opts);
|
return await createRuntimeApp<Env>(appConfig, ctx?.env);
|
||||||
}
|
}
|
||||||
|
|
||||||
// compatiblity
|
// compatiblity
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
/// <reference types="@cloudflare/workers-types" />
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
import { describe, test, expect } from "vitest";
|
import { describe, beforeAll, afterAll } from "vitest";
|
||||||
|
|
||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
import { Miniflare } from "miniflare";
|
import { Miniflare } from "miniflare";
|
||||||
import { doSqlite } from "./DoConnection";
|
import { doSqlite } from "./DoConnection";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
const script = `
|
const script = `
|
||||||
import { DurableObject } from "cloudflare:workers";
|
import { DurableObject } from "cloudflare:workers";
|
||||||
@@ -40,6 +41,9 @@ export default {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(() => enableConsoleLog());
|
||||||
|
|
||||||
describe("doSqlite", async () => {
|
describe("doSqlite", async () => {
|
||||||
connectionTestSuite(viTestRunner, {
|
connectionTestSuite(viTestRunner, {
|
||||||
makeConnection: async () => {
|
makeConnection: async () => {
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { cacheWorkersKV } from "./cache";
|
|||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
import { cacheDriverTestSuite } from "core/drivers/cache/cache-driver-test-suite";
|
import { cacheDriverTestSuite } from "core/drivers/cache/cache-driver-test-suite";
|
||||||
import { Miniflare } from "miniflare";
|
import { Miniflare } from "miniflare";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(() => enableConsoleLog());
|
||||||
|
|
||||||
describe("cacheWorkersKV", async () => {
|
describe("cacheWorkersKV", async () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import {
|
|||||||
type CloudflareBkndConfig,
|
type CloudflareBkndConfig,
|
||||||
type CloudflareEnv,
|
type CloudflareEnv,
|
||||||
} from "bknd/adapter/cloudflare";
|
} from "bknd/adapter/cloudflare";
|
||||||
import type { PlatformProxy } from "wrangler";
|
import type { GetPlatformProxyOptions, PlatformProxy } from "wrangler";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
|
import { $console } from "bknd/utils";
|
||||||
|
|
||||||
export type WithPlatformProxyOptions = {
|
export type WithPlatformProxyOptions = {
|
||||||
/**
|
/**
|
||||||
@@ -14,22 +15,27 @@ export type WithPlatformProxyOptions = {
|
|||||||
* You can override/force this by setting this option.
|
* You can override/force this by setting this option.
|
||||||
*/
|
*/
|
||||||
useProxy?: boolean;
|
useProxy?: boolean;
|
||||||
|
proxyOptions?: GetPlatformProxyOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function withPlatformProxy<Env extends CloudflareEnv>(
|
export function withPlatformProxy<Env extends CloudflareEnv>(
|
||||||
config?: CloudflareBkndConfig<Env>,
|
config: CloudflareBkndConfig<Env> = {},
|
||||||
opts?: WithPlatformProxyOptions,
|
opts?: WithPlatformProxyOptions,
|
||||||
) {
|
) {
|
||||||
const use_proxy =
|
const use_proxy =
|
||||||
typeof opts?.useProxy === "boolean" ? opts.useProxy : process.env.PROXY === "1";
|
typeof opts?.useProxy === "boolean" ? opts.useProxy : process.env.PROXY === "1";
|
||||||
let proxy: PlatformProxy | undefined;
|
let proxy: PlatformProxy | undefined;
|
||||||
|
|
||||||
|
$console.log("Using cloudflare platform proxy");
|
||||||
|
|
||||||
async function getEnv(env?: Env): Promise<Env> {
|
async function getEnv(env?: Env): Promise<Env> {
|
||||||
if (use_proxy) {
|
if (use_proxy) {
|
||||||
if (!proxy) {
|
if (!proxy) {
|
||||||
const getPlatformProxy = await import("wrangler").then((mod) => mod.getPlatformProxy);
|
const getPlatformProxy = await import("wrangler").then((mod) => mod.getPlatformProxy);
|
||||||
proxy = await getPlatformProxy();
|
proxy = await getPlatformProxy(opts?.proxyOptions);
|
||||||
setTimeout(proxy?.dispose, 1000);
|
process.on("exit", () => {
|
||||||
|
proxy?.dispose();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return proxy.env as unknown as Env;
|
return proxy.env as unknown as Env;
|
||||||
}
|
}
|
||||||
@@ -50,16 +56,22 @@ export function withPlatformProxy<Env extends CloudflareEnv>(
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
app: async (_env) => {
|
app: async (_env) => {
|
||||||
const env = await getEnv(_env);
|
const env = await getEnv(_env);
|
||||||
|
const binding = use_proxy ? getBinding(env, "D1Database") : undefined;
|
||||||
|
|
||||||
if (config?.app === undefined && use_proxy) {
|
if (config?.app === undefined && use_proxy && binding) {
|
||||||
const binding = getBinding(env, "D1Database");
|
|
||||||
return {
|
return {
|
||||||
connection: d1Sqlite({
|
connection: d1Sqlite({
|
||||||
binding: binding.value,
|
binding: binding.value,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
} else if (typeof config?.app === "function") {
|
} else if (typeof config?.app === "function") {
|
||||||
return config?.app(env);
|
const appConfig = await config?.app(env);
|
||||||
|
if (binding) {
|
||||||
|
appConfig.connection = d1Sqlite({
|
||||||
|
binding: binding.value,
|
||||||
|
}) as any;
|
||||||
|
}
|
||||||
|
return appConfig;
|
||||||
}
|
}
|
||||||
return config?.app || {};
|
return config?.app || {};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import { Miniflare } from "miniflare";
|
|||||||
import { StorageR2Adapter } from "./StorageR2Adapter";
|
import { StorageR2Adapter } from "./StorageR2Adapter";
|
||||||
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, afterAll, test, expect } from "vitest";
|
import { describe, afterAll, test, expect, beforeAll } from "vitest";
|
||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(() => enableConsoleLog());
|
||||||
|
|
||||||
let mf: Miniflare | undefined;
|
let mf: Miniflare | undefined;
|
||||||
describe("StorageR2Adapter", async () => {
|
describe("StorageR2Adapter", async () => {
|
||||||
|
|||||||
@@ -24,45 +24,157 @@ export function devFsVitePlugin({
|
|||||||
projectRoot = config.root;
|
projectRoot = config.root;
|
||||||
},
|
},
|
||||||
configureServer(server) {
|
configureServer(server) {
|
||||||
if (!isDev) return;
|
if (!isDev) {
|
||||||
|
verbose && console.debug("[dev-fs-plugin] Not in dev mode, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track active chunked requests
|
||||||
|
const activeRequests = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
totalChunks: number;
|
||||||
|
filename: string;
|
||||||
|
chunks: string[];
|
||||||
|
receivedChunks: number;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
// Intercept stdout to watch for our write requests
|
// Intercept stdout to watch for our write requests
|
||||||
const originalStdoutWrite = process.stdout.write;
|
const originalStdoutWrite = process.stdout.write;
|
||||||
process.stdout.write = function (chunk: any, encoding?: any, callback?: any) {
|
process.stdout.write = function (chunk: any, encoding?: any, callback?: any) {
|
||||||
const output = chunk.toString();
|
const output = chunk.toString();
|
||||||
|
|
||||||
// Check if this output contains our special write request
|
// Skip our own debug output
|
||||||
if (output.includes("{{DEV_FS_WRITE_REQUEST}}")) {
|
if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) {
|
||||||
try {
|
// @ts-ignore
|
||||||
// Extract the JSON from the log line
|
// biome-ignore lint/style/noArguments: <explanation>
|
||||||
const match = output.match(/{{DEV_FS_WRITE_REQUEST}} ({.*})/);
|
return originalStdoutWrite.apply(process.stdout, arguments);
|
||||||
if (match) {
|
}
|
||||||
const writeRequest = JSON.parse(match[1]);
|
|
||||||
if (writeRequest.type === "DEV_FS_WRITE_REQUEST") {
|
|
||||||
if (verbose) {
|
|
||||||
console.debug("[dev-fs-plugin] Intercepted write request via stdout");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the write request immediately
|
// Track if we process any protocol messages (to suppress output)
|
||||||
(async () => {
|
let processedProtocolMessage = false;
|
||||||
try {
|
|
||||||
const fullPath = resolve(projectRoot, writeRequest.filename);
|
|
||||||
await nodeWriteFile(fullPath, writeRequest.data);
|
|
||||||
if (verbose) {
|
|
||||||
console.debug("[dev-fs-plugin] File written successfully!");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[dev-fs-plugin] Error writing file:", error);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Don't output the raw write request to console
|
// Process all start markers in this output
|
||||||
return true;
|
if (output.includes("{{DEV_FS_START}}")) {
|
||||||
|
const startMatches = [
|
||||||
|
...output.matchAll(/{{DEV_FS_START}} ([a-z0-9]+) (\d+) (.+)/g),
|
||||||
|
];
|
||||||
|
for (const startMatch of startMatches) {
|
||||||
|
const requestId = startMatch[1];
|
||||||
|
const totalChunks = Number.parseInt(startMatch[2]);
|
||||||
|
const filename = startMatch[3];
|
||||||
|
|
||||||
|
activeRequests.set(requestId, {
|
||||||
|
totalChunks,
|
||||||
|
filename,
|
||||||
|
chunks: new Array(totalChunks),
|
||||||
|
receivedChunks: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
verbose &&
|
||||||
|
console.debug(
|
||||||
|
`[dev-fs-plugin] Started request ${requestId} for ${filename} (${totalChunks} chunks)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
processedProtocolMessage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all chunk data in this output
|
||||||
|
if (output.includes("{{DEV_FS_CHUNK}}")) {
|
||||||
|
const chunkMatches = [
|
||||||
|
...output.matchAll(/{{DEV_FS_CHUNK}} ([a-z0-9]+) (\d+) ([A-Za-z0-9+/=]+)/g),
|
||||||
|
];
|
||||||
|
for (const chunkMatch of chunkMatches) {
|
||||||
|
const requestId = chunkMatch[1];
|
||||||
|
const chunkIndex = Number.parseInt(chunkMatch[2]);
|
||||||
|
const chunkData = chunkMatch[3];
|
||||||
|
|
||||||
|
const request = activeRequests.get(requestId);
|
||||||
|
if (request) {
|
||||||
|
request.chunks[chunkIndex] = chunkData;
|
||||||
|
request.receivedChunks++;
|
||||||
|
verbose &&
|
||||||
|
console.debug(
|
||||||
|
`[dev-fs-plugin] Received chunk ${chunkIndex}/${request.totalChunks - 1} for ${request.filename} (length: ${chunkData.length})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate base64 chunk
|
||||||
|
if (chunkData.length < 1000 && chunkIndex < request.totalChunks - 1) {
|
||||||
|
verbose &&
|
||||||
|
console.warn(
|
||||||
|
`[dev-fs-plugin] WARNING: Chunk ${chunkIndex} seems truncated (length: ${chunkData.length})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
// Not a valid write request, continue with normal output
|
|
||||||
}
|
}
|
||||||
|
processedProtocolMessage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all end markers in this output
|
||||||
|
if (output.includes("{{DEV_FS_END}}")) {
|
||||||
|
const endMatches = [...output.matchAll(/{{DEV_FS_END}} ([a-z0-9]+)/g)];
|
||||||
|
for (const endMatch of endMatches) {
|
||||||
|
const requestId = endMatch[1];
|
||||||
|
const request = activeRequests.get(requestId);
|
||||||
|
|
||||||
|
if (request && request.receivedChunks === request.totalChunks) {
|
||||||
|
try {
|
||||||
|
// Reconstruct the base64 string
|
||||||
|
const fullBase64 = request.chunks.join("");
|
||||||
|
verbose &&
|
||||||
|
console.debug(
|
||||||
|
`[dev-fs-plugin] Reconstructed ${request.filename} - base64 length: ${fullBase64.length}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decode and parse
|
||||||
|
const decodedJson = atob(fullBase64);
|
||||||
|
const writeRequest = JSON.parse(decodedJson);
|
||||||
|
|
||||||
|
if (writeRequest.type === "DEV_FS_WRITE_REQUEST") {
|
||||||
|
verbose &&
|
||||||
|
console.debug(
|
||||||
|
`[dev-fs-plugin] Processing write request for ${writeRequest.filename}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process the write request
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const fullPath = resolve(projectRoot, writeRequest.filename);
|
||||||
|
verbose &&
|
||||||
|
console.debug(`[dev-fs-plugin] Writing to: ${fullPath}`);
|
||||||
|
await nodeWriteFile(fullPath, writeRequest.data);
|
||||||
|
verbose &&
|
||||||
|
console.debug("[dev-fs-plugin] File written successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[dev-fs-plugin] Error writing file:", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
activeRequests.delete(requestId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[dev-fs-plugin] Error processing chunked request:",
|
||||||
|
String(error),
|
||||||
|
);
|
||||||
|
activeRequests.delete(requestId);
|
||||||
|
}
|
||||||
|
} else if (request) {
|
||||||
|
verbose &&
|
||||||
|
console.debug(
|
||||||
|
`[dev-fs-plugin] Request ${requestId} incomplete: ${request.receivedChunks}/${request.totalChunks} chunks`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processedProtocolMessage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we processed any protocol messages, suppress output
|
||||||
|
if (processedProtocolMessage) {
|
||||||
|
return callback ? callback() : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -78,7 +190,10 @@ export function devFsVitePlugin({
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
transform(code, id, options) {
|
transform(code, id, options) {
|
||||||
// Only transform in SSR mode during development
|
// Only transform in SSR mode during development
|
||||||
if (!isDev || !options?.ssr) return;
|
//if (!isDev || !options?.ssr) return;
|
||||||
|
if (!isDev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is the bknd config file
|
// Check if this is the bknd config file
|
||||||
if (id.includes(configFile)) {
|
if (id.includes(configFile)) {
|
||||||
@@ -92,7 +207,7 @@ export function devFsVitePlugin({
|
|||||||
if (typeof globalThis !== 'undefined') {
|
if (typeof globalThis !== 'undefined') {
|
||||||
globalThis.__devFsPolyfill = {
|
globalThis.__devFsPolyfill = {
|
||||||
writeFile: async (filename, data) => {
|
writeFile: async (filename, data) => {
|
||||||
${verbose ? "console.debug('dev-fs polyfill: Intercepting write request for', filename);" : ""}
|
${verbose ? "console.debug('[dev-fs-polyfill] Intercepting write request for', filename);" : ""}
|
||||||
|
|
||||||
// Use console logging as a communication channel
|
// Use console logging as a communication channel
|
||||||
// The main process will watch for this specific log pattern
|
// The main process will watch for this specific log pattern
|
||||||
@@ -103,16 +218,38 @@ if (typeof globalThis !== 'undefined') {
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Output as a specially formatted console message
|
// Output as a specially formatted console message with end delimiter
|
||||||
console.log('{{DEV_FS_WRITE_REQUEST}}', JSON.stringify(writeRequest));
|
// Base64 encode the JSON to avoid any control character issues
|
||||||
${verbose ? "console.debug('dev-fs polyfill: Write request sent via console');" : ""}
|
const jsonString = JSON.stringify(writeRequest);
|
||||||
|
const encodedJson = btoa(jsonString);
|
||||||
|
|
||||||
|
// Split into reasonable chunks that balance performance vs reliability
|
||||||
|
const chunkSize = 2000; // 2KB chunks - safe for most environments
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < encodedJson.length; i += chunkSize) {
|
||||||
|
chunks.push(encodedJson.slice(i, i + chunkSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
|
||||||
|
|
||||||
|
// Send start marker (use stdout.write to avoid console display)
|
||||||
|
process.stdout.write('{{DEV_FS_START}} ' + requestId + ' ' + chunks.length + ' ' + filename + '\\n');
|
||||||
|
|
||||||
|
// Send each chunk
|
||||||
|
chunks.forEach((chunk, index) => {
|
||||||
|
process.stdout.write('{{DEV_FS_CHUNK}} ' + requestId + ' ' + index + ' ' + chunk + '\\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send end marker
|
||||||
|
process.stdout.write('{{DEV_FS_END}} ' + requestId + '\\n');
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}`;
|
||||||
`;
|
|
||||||
return polyfill + code;
|
return polyfill + code;
|
||||||
|
} else {
|
||||||
|
verbose && console.debug("[dev-fs-plugin] Not transforming", id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} satisfies Plugin;
|
} satisfies Plugin;
|
||||||
|
|||||||
+19
-38
@@ -13,21 +13,14 @@ import type { AdminControllerOptions } from "modules/server/AdminController";
|
|||||||
import type { Manifest } from "vite";
|
import type { Manifest } from "vite";
|
||||||
|
|
||||||
export type BkndConfig<Args = any> = CreateAppConfig & {
|
export type BkndConfig<Args = any> = CreateAppConfig & {
|
||||||
app?: CreateAppConfig | ((args: Args) => MaybePromise<CreateAppConfig>);
|
app?: Omit<BkndConfig, "app"> | ((args: Args) => MaybePromise<Omit<BkndConfig<Args>, "app">>);
|
||||||
onBuilt?: (app: App) => Promise<void>;
|
onBuilt?: (app: App) => MaybePromise<void>;
|
||||||
beforeBuild?: (app: App, registries?: typeof $registries) => Promise<void>;
|
beforeBuild?: (app?: App, registries?: typeof $registries) => MaybePromise<void>;
|
||||||
buildConfig?: Parameters<App["build"]>[0];
|
buildConfig?: Parameters<App["build"]>[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
|
export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
|
||||||
|
|
||||||
export type CreateAdapterAppOptions = {
|
|
||||||
force?: boolean;
|
|
||||||
id?: string;
|
|
||||||
};
|
|
||||||
export type FrameworkOptions = CreateAdapterAppOptions;
|
|
||||||
export type RuntimeOptions = CreateAdapterAppOptions;
|
|
||||||
|
|
||||||
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
|
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
|
||||||
distPath?: string;
|
distPath?: string;
|
||||||
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||||
@@ -41,7 +34,7 @@ export type DefaultArgs = {
|
|||||||
export async function makeConfig<Args = DefaultArgs>(
|
export async function makeConfig<Args = DefaultArgs>(
|
||||||
config: BkndConfig<Args>,
|
config: BkndConfig<Args>,
|
||||||
args?: Args,
|
args?: Args,
|
||||||
): Promise<CreateAppConfig> {
|
): Promise<Omit<BkndConfig<Args>, "app">> {
|
||||||
let additionalConfig: CreateAppConfig = {};
|
let additionalConfig: CreateAppConfig = {};
|
||||||
const { app, ...rest } = config;
|
const { app, ...rest } = config;
|
||||||
if (app) {
|
if (app) {
|
||||||
@@ -59,45 +52,34 @@ export async function makeConfig<Args = DefaultArgs>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// a map that contains all apps by id
|
// a map that contains all apps by id
|
||||||
const apps = new Map<string, App>();
|
|
||||||
export async function createAdapterApp<Config extends BkndConfig = BkndConfig, Args = DefaultArgs>(
|
export async function createAdapterApp<Config extends BkndConfig = BkndConfig, Args = DefaultArgs>(
|
||||||
config: Config = {} as Config,
|
config: Config = {} as Config,
|
||||||
args?: Args,
|
args?: Args,
|
||||||
opts?: CreateAdapterAppOptions,
|
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
const id = opts?.id ?? "app";
|
await config.beforeBuild?.(undefined, $registries);
|
||||||
let app = apps.get(id);
|
|
||||||
if (!app || opts?.force) {
|
|
||||||
const appConfig = await makeConfig(config, args);
|
|
||||||
if (!appConfig.connection || !Connection.isConnection(appConfig.connection)) {
|
|
||||||
let connection: Connection | undefined;
|
|
||||||
if (Connection.isConnection(config.connection)) {
|
|
||||||
connection = config.connection;
|
|
||||||
} else {
|
|
||||||
const sqlite = (await import("bknd/adapter/sqlite")).sqlite;
|
|
||||||
const conf = appConfig.connection ?? { url: ":memory:" };
|
|
||||||
connection = sqlite(conf) as any;
|
|
||||||
$console.info(`Using ${connection!.name} connection`, conf.url);
|
|
||||||
}
|
|
||||||
appConfig.connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
app = App.create(appConfig);
|
const appConfig = await makeConfig(config, args);
|
||||||
|
if (!appConfig.connection || !Connection.isConnection(appConfig.connection)) {
|
||||||
if (!opts?.force) {
|
let connection: Connection | undefined;
|
||||||
apps.set(id, app);
|
if (Connection.isConnection(config.connection)) {
|
||||||
|
connection = config.connection;
|
||||||
|
} else {
|
||||||
|
const sqlite = (await import("bknd/adapter/sqlite")).sqlite;
|
||||||
|
const conf = appConfig.connection ?? { url: ":memory:" };
|
||||||
|
connection = sqlite(conf) as any;
|
||||||
|
$console.info(`Using ${connection!.name} connection`, conf.url);
|
||||||
}
|
}
|
||||||
|
appConfig.connection = connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return App.create(appConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFrameworkApp<Args = DefaultArgs>(
|
export async function createFrameworkApp<Args = DefaultArgs>(
|
||||||
config: FrameworkBkndConfig = {},
|
config: FrameworkBkndConfig = {},
|
||||||
args?: Args,
|
args?: Args,
|
||||||
opts?: FrameworkOptions,
|
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
const app = await createAdapterApp(config, args, opts);
|
const app = await createAdapterApp(config, args);
|
||||||
|
|
||||||
if (!app.isBuilt()) {
|
if (!app.isBuilt()) {
|
||||||
if (config.onBuilt) {
|
if (config.onBuilt) {
|
||||||
@@ -120,9 +102,8 @@ export async function createFrameworkApp<Args = DefaultArgs>(
|
|||||||
export async function createRuntimeApp<Args = DefaultArgs>(
|
export async function createRuntimeApp<Args = DefaultArgs>(
|
||||||
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig<Args> = {},
|
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig<Args> = {},
|
||||||
args?: Args,
|
args?: Args,
|
||||||
opts?: RuntimeOptions,
|
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
const app = await createAdapterApp(config, args, opts);
|
const app = await createAdapterApp(config, args);
|
||||||
|
|
||||||
if (!app.isBuilt()) {
|
if (!app.isBuilt()) {
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createFrameworkApp, type FrameworkBkndConfig, type FrameworkOptions } from "bknd/adapter";
|
import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
|
||||||
import { isNode } from "bknd/utils";
|
import { isNode } from "bknd/utils";
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
@@ -10,9 +10,8 @@ export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
|
|||||||
export async function getApp<Env = NextjsEnv>(
|
export async function getApp<Env = NextjsEnv>(
|
||||||
config: NextjsBkndConfig<Env>,
|
config: NextjsBkndConfig<Env>,
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: FrameworkOptions,
|
|
||||||
) {
|
) {
|
||||||
return await createFrameworkApp(config, args ?? (process.env as Env), opts);
|
return await createFrameworkApp(config, args ?? (process.env as Env));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
|
function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) {
|
||||||
@@ -41,10 +40,9 @@ function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequ
|
|||||||
export function serve<Env = NextjsEnv>(
|
export function serve<Env = NextjsEnv>(
|
||||||
{ cleanRequest, ...config }: NextjsBkndConfig<Env> = {},
|
{ cleanRequest, ...config }: NextjsBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: FrameworkOptions,
|
|
||||||
) {
|
) {
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
const app = await getApp(config, args, opts);
|
const app = await getApp(config, args);
|
||||||
const request = getCleanRequest(req, cleanRequest);
|
const request = getCleanRequest(req, cleanRequest);
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import { genericSqlite } from "bknd";
|
import {
|
||||||
|
genericSqlite,
|
||||||
|
type GenericSqliteConnection,
|
||||||
|
type GenericSqliteConnectionConfig,
|
||||||
|
} from "bknd";
|
||||||
import { DatabaseSync } from "node:sqlite";
|
import { DatabaseSync } from "node:sqlite";
|
||||||
|
import { omitKeys } from "bknd/utils";
|
||||||
|
|
||||||
export type NodeSqliteConnectionConfig = {
|
export type NodeSqliteConnection = GenericSqliteConnection<DatabaseSync>;
|
||||||
database: DatabaseSync;
|
export type NodeSqliteConnectionConfig = Omit<
|
||||||
};
|
GenericSqliteConnectionConfig<DatabaseSync>,
|
||||||
|
"name" | "supports"
|
||||||
|
> &
|
||||||
|
({ database?: DatabaseSync; url?: never } | { url?: string; database?: never });
|
||||||
|
|
||||||
export function nodeSqlite(config?: NodeSqliteConnectionConfig | { url: string }) {
|
export function nodeSqlite(config?: NodeSqliteConnectionConfig) {
|
||||||
let db: DatabaseSync;
|
let db: DatabaseSync | undefined;
|
||||||
if (config) {
|
if (config) {
|
||||||
if ("database" in config) {
|
if ("database" in config && config.database) {
|
||||||
db = config.database;
|
db = config.database;
|
||||||
} else {
|
} else if (config.url) {
|
||||||
db = new DatabaseSync(config.url);
|
db = new DatabaseSync(config.url);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
db = new DatabaseSync(":memory:");
|
db = new DatabaseSync(":memory:");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +31,7 @@ export function nodeSqlite(config?: NodeSqliteConnectionConfig | { url: string }
|
|||||||
"node-sqlite",
|
"node-sqlite",
|
||||||
db,
|
db,
|
||||||
(utils) => {
|
(utils) => {
|
||||||
const getStmt = (sql: string) => {
|
const getStmt = (sql: string) => db.prepare(sql);
|
||||||
const stmt = db.prepare(sql);
|
|
||||||
//stmt.setReadBigInts(true);
|
|
||||||
return stmt;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
db,
|
db,
|
||||||
@@ -49,6 +55,7 @@ export function nodeSqlite(config?: NodeSqliteConnectionConfig | { url: string }
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
...omitKeys(config ?? ({} as any), ["database", "url", "name", "supports"]),
|
||||||
supports: {
|
supports: {
|
||||||
batching: false,
|
batching: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { nodeSqlite } from "./NodeSqliteConnection";
|
import { nodeSqlite } from "./NodeSqliteConnection";
|
||||||
import { DatabaseSync } from "node:sqlite";
|
import { DatabaseSync } from "node:sqlite";
|
||||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
import { describe } from "vitest";
|
import { describe, beforeAll, afterAll, test, expect, vi } from "vitest";
|
||||||
import { viTestRunner } from "../vitest";
|
import { viTestRunner } from "../vitest";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
import { GenericSqliteConnection } from "data/connection/sqlite/GenericSqliteConnection";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(() => enableConsoleLog());
|
||||||
|
|
||||||
describe("NodeSqliteConnection", () => {
|
describe("NodeSqliteConnection", () => {
|
||||||
connectionTestSuite(viTestRunner, {
|
connectionTestSuite(viTestRunner, {
|
||||||
@@ -12,4 +17,20 @@ describe("NodeSqliteConnection", () => {
|
|||||||
}),
|
}),
|
||||||
rawDialectDetails: [],
|
rawDialectDetails: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("onCreateConnection", async () => {
|
||||||
|
const called = vi.fn(() => null);
|
||||||
|
|
||||||
|
const conn = nodeSqlite({
|
||||||
|
onCreateConnection: (db) => {
|
||||||
|
expect(db).toBeInstanceOf(DatabaseSync);
|
||||||
|
called();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await conn.ping();
|
||||||
|
|
||||||
|
expect(conn).toBeInstanceOf(GenericSqliteConnection);
|
||||||
|
expect(conn.db).toBeInstanceOf(DatabaseSync);
|
||||||
|
expect(called).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "node:path";
|
|||||||
import { serve as honoServe } from "@hono/node-server";
|
import { serve as honoServe } from "@hono/node-server";
|
||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { registerLocalMediaAdapter } from "adapter/node/storage";
|
import { registerLocalMediaAdapter } from "adapter/node/storage";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||||
import { config as $config, type App } from "bknd";
|
import { config as $config, type App } from "bknd";
|
||||||
import { $console } from "bknd/utils";
|
import { $console } from "bknd/utils";
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ export type NodeBkndConfig<Env = NodeEnv> = RuntimeBkndConfig<Env> & {
|
|||||||
export async function createApp<Env = NodeEnv>(
|
export async function createApp<Env = NodeEnv>(
|
||||||
{ distPath, relativeDistPath, ...config }: NodeBkndConfig<Env> = {},
|
{ distPath, relativeDistPath, ...config }: NodeBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
|
||||||
) {
|
) {
|
||||||
const root = path.relative(
|
const root = path.relative(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
@@ -36,19 +35,17 @@ export async function createApp<Env = NodeEnv>(
|
|||||||
},
|
},
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
args ?? { env: process.env },
|
args ?? { env: process.env },
|
||||||
opts,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHandler<Env = NodeEnv>(
|
export function createHandler<Env = NodeEnv>(
|
||||||
config: NodeBkndConfig<Env> = {},
|
config: NodeBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
|
||||||
) {
|
) {
|
||||||
let app: App | undefined;
|
let app: App | undefined;
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = await createApp(config, args ?? (process.env as Env), opts);
|
app = await createApp(config, args ?? (process.env as Env));
|
||||||
}
|
}
|
||||||
return app.fetch(req);
|
return app.fetch(req);
|
||||||
};
|
};
|
||||||
@@ -57,13 +54,12 @@ export function createHandler<Env = NodeEnv>(
|
|||||||
export function serve<Env = NodeEnv>(
|
export function serve<Env = NodeEnv>(
|
||||||
{ port = $config.server.default_port, hostname, listener, ...config }: NodeBkndConfig<Env> = {},
|
{ port = $config.server.default_port, hostname, listener, ...config }: NodeBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: RuntimeOptions,
|
|
||||||
) {
|
) {
|
||||||
honoServe(
|
honoServe(
|
||||||
{
|
{
|
||||||
port,
|
port,
|
||||||
hostname,
|
hostname,
|
||||||
fetch: createHandler(config, args, opts),
|
fetch: createHandler(config, args),
|
||||||
},
|
},
|
||||||
(connInfo) => {
|
(connInfo) => {
|
||||||
$console.log(`Server is running on http://localhost:${connInfo.port}`);
|
$console.log(`Server is running on http://localhost:${connInfo.port}`);
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import { describe, beforeAll, afterAll } from "vitest";
|
|||||||
import * as node from "./node.adapter";
|
import * as node from "./node.adapter";
|
||||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("node adapter", () => {
|
describe("node adapter", () => {
|
||||||
adapterTestSuite(viTestRunner, {
|
adapterTestSuite(viTestRunner, {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { describe } from "vitest";
|
import { describe, beforeAll, afterAll } from "vitest";
|
||||||
import { viTestRunner } from "adapter/node/vitest";
|
import { viTestRunner } from "adapter/node/vitest";
|
||||||
import { StorageLocalAdapter } from "adapter/node";
|
import { StorageLocalAdapter } from "adapter/node";
|
||||||
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(() => enableConsoleLog());
|
||||||
|
|
||||||
describe("StorageLocalAdapter (node)", async () => {
|
describe("StorageLocalAdapter (node)", async () => {
|
||||||
const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets");
|
const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import nodeAssert from "node:assert/strict";
|
import nodeAssert from "node:assert/strict";
|
||||||
import { test, describe, beforeEach, afterEach } from "node:test";
|
import { test, describe, beforeEach, afterEach, after, before } from "node:test";
|
||||||
import type { Matcher, Test, TestFn, TestRunner } from "core/test";
|
import type { Matcher, Test, TestFn, TestRunner } from "core/test";
|
||||||
|
|
||||||
// Track mock function calls
|
// Track mock function calls
|
||||||
@@ -99,5 +99,6 @@ export const nodeTestRunner: TestRunner = {
|
|||||||
}),
|
}),
|
||||||
beforeEach: beforeEach,
|
beforeEach: beforeEach,
|
||||||
afterEach: afterEach,
|
afterEach: afterEach,
|
||||||
afterAll: () => {},
|
afterAll: after,
|
||||||
|
beforeAll: before,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { TestFn, TestRunner, Test } from "core/test";
|
import type { TestFn, TestRunner, Test } from "core/test";
|
||||||
import { describe, test, expect, vi, beforeEach, afterEach, afterAll } from "vitest";
|
import { describe, test, expect, vi, beforeEach, afterEach, afterAll, beforeAll } from "vitest";
|
||||||
|
|
||||||
function vitestTest(label: string, fn: TestFn, options?: any) {
|
function vitestTest(label: string, fn: TestFn, options?: any) {
|
||||||
return test(label, fn as any);
|
return test(label, fn as any);
|
||||||
@@ -50,4 +50,5 @@ export const viTestRunner: TestRunner = {
|
|||||||
beforeEach: beforeEach,
|
beforeEach: beforeEach,
|
||||||
afterEach: afterEach,
|
afterEach: afterEach,
|
||||||
afterAll: afterAll,
|
afterAll: afterAll,
|
||||||
|
beforeAll: beforeAll,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ afterAll(enableConsoleLog);
|
|||||||
describe("react-router adapter", () => {
|
describe("react-router adapter", () => {
|
||||||
adapterTestSuite(bunTestRunner, {
|
adapterTestSuite(bunTestRunner, {
|
||||||
makeApp: rr.getApp,
|
makeApp: rr.getApp,
|
||||||
makeHandler: (c, a, o) => (request: Request) => rr.serve(c, a?.env, o)({ request }),
|
makeHandler: (c, a) => (request: Request) => rr.serve(c, a?.env)({ request }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||||
import type { FrameworkOptions } from "adapter";
|
|
||||||
|
|
||||||
type ReactRouterEnv = NodeJS.ProcessEnv;
|
type ReactRouterEnv = NodeJS.ProcessEnv;
|
||||||
type ReactRouterFunctionArgs = {
|
type ReactRouterFunctionArgs = {
|
||||||
@@ -10,17 +9,15 @@ export type ReactRouterBkndConfig<Env = ReactRouterEnv> = FrameworkBkndConfig<En
|
|||||||
export async function getApp<Env = ReactRouterEnv>(
|
export async function getApp<Env = ReactRouterEnv>(
|
||||||
config: ReactRouterBkndConfig<Env>,
|
config: ReactRouterBkndConfig<Env>,
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: FrameworkOptions,
|
|
||||||
) {
|
) {
|
||||||
return await createFrameworkApp(config, args ?? process.env, opts);
|
return await createFrameworkApp(config, args ?? process.env);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<Env = ReactRouterEnv>(
|
export function serve<Env = ReactRouterEnv>(
|
||||||
config: ReactRouterBkndConfig<Env> = {},
|
config: ReactRouterBkndConfig<Env> = {},
|
||||||
args: Env = {} as Env,
|
args: Env = {} as Env,
|
||||||
opts?: FrameworkOptions,
|
|
||||||
) {
|
) {
|
||||||
return async (fnArgs: ReactRouterFunctionArgs) => {
|
return async (fnArgs: ReactRouterFunctionArgs) => {
|
||||||
return (await getApp(config, args, opts)).fetch(fnArgs.request);
|
return (await getApp(config, args)).fetch(fnArgs.request);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { type DevServerOptions, default as honoViteDevServer } from "@hono/vite-dev-server";
|
import { type DevServerOptions, default as honoViteDevServer } from "@hono/vite-dev-server";
|
||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { type RuntimeBkndConfig, createRuntimeApp, type FrameworkOptions } from "bknd/adapter";
|
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||||
import { devServerConfig } from "./dev-server-config";
|
import { devServerConfig } from "./dev-server-config";
|
||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
@@ -30,7 +30,6 @@ ${addBkndContext ? "<!-- BKND_CONTEXT -->" : ""}
|
|||||||
async function createApp<ViteEnv>(
|
async function createApp<ViteEnv>(
|
||||||
config: ViteBkndConfig<ViteEnv> = {},
|
config: ViteBkndConfig<ViteEnv> = {},
|
||||||
env: ViteEnv = {} as ViteEnv,
|
env: ViteEnv = {} as ViteEnv,
|
||||||
opts: FrameworkOptions = {},
|
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
registerLocalMediaAdapter();
|
registerLocalMediaAdapter();
|
||||||
return await createRuntimeApp(
|
return await createRuntimeApp(
|
||||||
@@ -47,18 +46,13 @@ async function createApp<ViteEnv>(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
env,
|
env,
|
||||||
opts,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<ViteEnv>(
|
export function serve<ViteEnv>(config: ViteBkndConfig<ViteEnv> = {}, args?: ViteEnv) {
|
||||||
config: ViteBkndConfig<ViteEnv> = {},
|
|
||||||
args?: ViteEnv,
|
|
||||||
opts?: FrameworkOptions,
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
||||||
const app = await createApp(config, env, opts);
|
const app = await createApp(config, env);
|
||||||
return app.fetch(request, env, ctx);
|
return app.fetch(request, env, ctx);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -278,7 +278,9 @@ export class Authenticator<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return payload as any;
|
return payload as any;
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
$console.debug("Authenticator jwt verify error", String(e));
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -396,8 +398,9 @@ export class Authenticator<
|
|||||||
if (headers.has("Authorization")) {
|
if (headers.has("Authorization")) {
|
||||||
const bearerHeader = String(headers.get("Authorization"));
|
const bearerHeader = String(headers.get("Authorization"));
|
||||||
token = bearerHeader.replace("Bearer ", "");
|
token = bearerHeader.replace("Bearer ", "");
|
||||||
} else if (is_context) {
|
} else {
|
||||||
token = await this.getAuthCookie(c as Context);
|
const context = is_context ? (c as Context) : ({ req: { raw: { headers } } } as Context);
|
||||||
|
token = await this.getAuthCookie(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { makeAppFromEnv } from "cli/commands/run";
|
|||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import c from "picocolors";
|
import c from "picocolors";
|
||||||
import { withConfigOptions } from "cli/utils/options";
|
import { withConfigOptions } from "cli/utils/options";
|
||||||
|
import { $console } from "bknd/utils";
|
||||||
|
|
||||||
export const config: CliCommand = (program) => {
|
export const config: CliCommand = (program) => {
|
||||||
withConfigOptions(program.command("config"))
|
withConfigOptions(program.command("config"))
|
||||||
@@ -19,7 +20,14 @@ export const config: CliCommand = (program) => {
|
|||||||
config = getDefaultConfig();
|
config = getDefaultConfig();
|
||||||
} else {
|
} else {
|
||||||
const app = await makeAppFromEnv(options);
|
const app = await makeAppFromEnv(options);
|
||||||
config = app.toJSON(options.secrets);
|
const manager = app.modules;
|
||||||
|
|
||||||
|
if (options.secrets) {
|
||||||
|
$console.warn("Including secrets in output");
|
||||||
|
config = manager.toJSON(true);
|
||||||
|
} else {
|
||||||
|
config = manager.extractSecrets().configs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config = options.pretty ? JSON.stringify(config, null, 2) : JSON.stringify(config);
|
config = options.pretty ? JSON.stringify(config, null, 2) : JSON.stringify(config);
|
||||||
@@ -31,5 +39,7 @@ export const config: CliCommand = (program) => {
|
|||||||
} else {
|
} else {
|
||||||
console.info(JSON.parse(config));
|
console.info(JSON.parse(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,4 +34,5 @@ async function action(options: { out?: string; clean?: boolean }) {
|
|||||||
|
|
||||||
// biome-ignore lint/suspicious/noConsoleLog:
|
// biome-ignore lint/suspicious/noConsoleLog:
|
||||||
console.log(c.green(`Assets copied to: ${c.bold(out)}`));
|
console.log(c.green(`Assets copied to: ${c.bold(out)}`));
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ const subjects = {
|
|||||||
async function action(subject: string) {
|
async function action(subject: string) {
|
||||||
if (subject in subjects) {
|
if (subject in subjects) {
|
||||||
await subjects[subject]();
|
await subjects[subject]();
|
||||||
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
console.error("Invalid subject: ", subject);
|
console.error("Invalid subject: ", subject);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export { copyAssets } from "./copy-assets";
|
|||||||
export { types } from "./types";
|
export { types } from "./types";
|
||||||
export { mcp } from "./mcp/mcp";
|
export { mcp } from "./mcp/mcp";
|
||||||
export { sync } from "./sync";
|
export { sync } from "./sync";
|
||||||
|
export { secrets } from "./secrets";
|
||||||
|
|||||||
@@ -9,18 +9,28 @@ export const PLATFORMS = ["node", "bun"] as const;
|
|||||||
export type Platform = (typeof PLATFORMS)[number];
|
export type Platform = (typeof PLATFORMS)[number];
|
||||||
|
|
||||||
export async function serveStatic(server: Platform): Promise<MiddlewareHandler> {
|
export async function serveStatic(server: Platform): Promise<MiddlewareHandler> {
|
||||||
|
const onNotFound = (path: string) => {
|
||||||
|
$console.debug("Couldn't resolve static file at", path);
|
||||||
|
};
|
||||||
|
|
||||||
switch (server) {
|
switch (server) {
|
||||||
case "node": {
|
case "node": {
|
||||||
const m = await import("@hono/node-server/serve-static");
|
const m = await import("@hono/node-server/serve-static");
|
||||||
|
const root = getRelativeDistPath() + "/static";
|
||||||
|
$console.log("Serving static files from", root);
|
||||||
return m.serveStatic({
|
return m.serveStatic({
|
||||||
// somehow different for node
|
// somehow different for node
|
||||||
root: getRelativeDistPath() + "/static",
|
root,
|
||||||
|
onNotFound,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case "bun": {
|
case "bun": {
|
||||||
const m = await import("hono/bun");
|
const m = await import("hono/bun");
|
||||||
|
const root = path.resolve(getRelativeDistPath(), "static");
|
||||||
|
$console.log("Serving static files from", root);
|
||||||
return m.serveStatic({
|
return m.serveStatic({
|
||||||
root: path.resolve(getRelativeDistPath(), "static"),
|
root,
|
||||||
|
onNotFound,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Config } from "@libsql/client/node";
|
|||||||
import { StorageLocalAdapter } from "adapter/node/storage";
|
import { StorageLocalAdapter } from "adapter/node/storage";
|
||||||
import type { CliBkndConfig, CliCommand } from "cli/types";
|
import type { CliBkndConfig, CliCommand } from "cli/types";
|
||||||
import { Option } from "commander";
|
import { Option } from "commander";
|
||||||
import { config, type App, type CreateAppConfig } from "bknd";
|
import { config, type App, type CreateAppConfig, type MaybePromise } from "bknd";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { registries } from "modules/registries";
|
import { registries } from "modules/registries";
|
||||||
import c from "picocolors";
|
import c from "picocolors";
|
||||||
@@ -60,7 +60,7 @@ type MakeAppConfig = {
|
|||||||
connection?: CreateAppConfig["connection"];
|
connection?: CreateAppConfig["connection"];
|
||||||
server?: { platform?: Platform };
|
server?: { platform?: Platform };
|
||||||
setAdminHtml?: boolean;
|
setAdminHtml?: boolean;
|
||||||
onBuilt?: (app: App) => Promise<void>;
|
onBuilt?: (app: App) => MaybePromise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function makeApp(config: MakeAppConfig) {
|
async function makeApp(config: MakeAppConfig) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const schema: CliCommand = (program) => {
|
|||||||
.option("--pretty", "pretty print")
|
.option("--pretty", "pretty print")
|
||||||
.action((options) => {
|
.action((options) => {
|
||||||
const schema = getDefaultSchema();
|
const schema = getDefaultSchema();
|
||||||
// biome-ignore lint/suspicious/noConsoleLog:
|
console.info(options.pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema));
|
||||||
console.log(options.pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema));
|
process.exit(0);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { CliCommand } from "../types";
|
||||||
|
import { makeAppFromEnv } from "cli/commands/run";
|
||||||
|
import { writeFile } from "node:fs/promises";
|
||||||
|
import c from "picocolors";
|
||||||
|
import { withConfigOptions, type WithConfigOptions } from "cli/utils/options";
|
||||||
|
import { transformObject } from "bknd/utils";
|
||||||
|
import { Option } from "commander";
|
||||||
|
|
||||||
|
export const secrets: CliCommand = (program) => {
|
||||||
|
withConfigOptions(program.command("secrets"))
|
||||||
|
.description("get app secrets")
|
||||||
|
.option("--template", "template output without the actual secrets")
|
||||||
|
.addOption(
|
||||||
|
new Option("--format <format>", "format output").choices(["json", "env"]).default("json"),
|
||||||
|
)
|
||||||
|
.option("--out <file>", "output file")
|
||||||
|
.action(
|
||||||
|
async (
|
||||||
|
options: WithConfigOptions<{ template: string; format: "json" | "env"; out: string }>,
|
||||||
|
) => {
|
||||||
|
const app = await makeAppFromEnv(options);
|
||||||
|
const manager = app.modules;
|
||||||
|
|
||||||
|
let secrets = manager.extractSecrets().secrets;
|
||||||
|
if (options.template) {
|
||||||
|
secrets = transformObject(secrets, () => "");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("");
|
||||||
|
if (options.out) {
|
||||||
|
if (options.format === "env") {
|
||||||
|
await writeFile(
|
||||||
|
options.out,
|
||||||
|
Object.entries(secrets)
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join("\n"),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await writeFile(options.out, JSON.stringify(secrets, null, 2));
|
||||||
|
}
|
||||||
|
console.info(`Secrets written to ${c.cyan(options.out)}`);
|
||||||
|
} else {
|
||||||
|
if (options.format === "env") {
|
||||||
|
console.info(
|
||||||
|
c.cyan(
|
||||||
|
Object.entries(secrets)
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join("\n"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.info(secrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info("");
|
||||||
|
process.exit(0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,13 +7,14 @@ import { withConfigOptions } from "cli/utils/options";
|
|||||||
export const sync: CliCommand = (program) => {
|
export const sync: CliCommand = (program) => {
|
||||||
withConfigOptions(program.command("sync"))
|
withConfigOptions(program.command("sync"))
|
||||||
.description("sync database")
|
.description("sync database")
|
||||||
.option("--dump", "dump operations to console instead of executing them")
|
.option("--force", "perform database syncing 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")
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
const app = await makeAppFromEnv(options);
|
const app = await makeAppFromEnv(options);
|
||||||
const schema = app.em.schema();
|
const schema = app.em.schema();
|
||||||
|
console.info(c.dim("Checking database state..."));
|
||||||
const stmts = await schema.sync({ drop: options.drop });
|
const stmts = await schema.sync({ drop: options.drop });
|
||||||
|
|
||||||
console.info("");
|
console.info("");
|
||||||
@@ -24,22 +25,27 @@ export const sync: CliCommand = (program) => {
|
|||||||
// @todo: currently assuming parameters aren't used
|
// @todo: currently assuming parameters aren't used
|
||||||
const sql = stmts.map((d) => d.sql).join(";\n") + ";";
|
const sql = stmts.map((d) => d.sql).join(";\n") + ";";
|
||||||
|
|
||||||
if (options.dump) {
|
if (options.force) {
|
||||||
|
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(`${c.green("Database synced")}`);
|
||||||
|
} 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);
|
||||||
await writeFile(options.out, output);
|
await writeFile(options.out, output);
|
||||||
console.info(`SQL written to ${c.cyan(options.out)}`);
|
console.info(`SQL written to ${c.cyan(options.out)}`);
|
||||||
} else {
|
} else {
|
||||||
console.info(options.sql ? c.cyan(sql) : stmts);
|
console.info(c.dim("DDL to execute:") + "\n" + c.cyan(sql));
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
c.yellow(
|
||||||
|
"\nNo statements have been executed. Use --force to perform database syncing operations",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
}
|
}
|
||||||
|
process.exit(0);
|
||||||
await schema.sync({ force: true, drop: options.drop });
|
|
||||||
console.info(c.cyan(sql));
|
|
||||||
|
|
||||||
console.info(`${c.gray(`Executed ${c.cyan(stmts.length)} statement(s)`)}`);
|
|
||||||
console.info(`${c.green("Database synced")}`);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,4 +35,6 @@ async function action({
|
|||||||
await writeFile(outfile, et.toString());
|
await writeFile(outfile, et.toString());
|
||||||
console.info(`\nTypes written to ${c.cyan(outfile)}`);
|
console.info(`\nTypes written to ${c.cyan(outfile)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,9 +78,11 @@ async function create(app: App, options: any) {
|
|||||||
password: await strategy.hash(password as string),
|
password: await strategy.hash(password as string),
|
||||||
});
|
});
|
||||||
$log.success(`Created user: ${c.cyan(created.email)}`);
|
$log.success(`Created user: ${c.cyan(created.email)}`);
|
||||||
|
process.exit(0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$log.error("Error creating user");
|
$log.error("Error creating user");
|
||||||
$console.error(e);
|
$console.error(e);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,8 +123,10 @@ async function update(app: App, options: any) {
|
|||||||
|
|
||||||
if (await app.module.auth.changePassword(user.id, password)) {
|
if (await app.module.auth.changePassword(user.id, password)) {
|
||||||
$log.success(`Updated user: ${c.cyan(user.email)}`);
|
$log.success(`Updated user: ${c.cyan(user.email)}`);
|
||||||
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
$log.error("Error updating user");
|
$log.error("Error updating user");
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,4 +162,5 @@ async function token(app: App, options: any) {
|
|||||||
console.log(
|
console.log(
|
||||||
`\n${c.dim("Token:")}\n${c.yellow(await app.module.auth.authenticator.jwt(user))}\n`,
|
`\n${c.dim("Token:")}\n${c.yellow(await app.module.auth.authenticator.jwt(user))}\n`,
|
||||||
);
|
);
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ export interface AppEntity<IdType = number | string> {
|
|||||||
|
|
||||||
export interface DB {
|
export interface DB {
|
||||||
// make sure to make unknown as "any"
|
// make sure to make unknown as "any"
|
||||||
[key: string]: {
|
/* [key: string]: {
|
||||||
id: PrimaryFieldType;
|
id: PrimaryFieldType;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
}; */
|
||||||
|
// @todo: that's not good, but required for admin options
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type TestRunner = {
|
|||||||
beforeEach: (fn: () => MaybePromise<void>) => void;
|
beforeEach: (fn: () => MaybePromise<void>) => void;
|
||||||
afterEach: (fn: () => MaybePromise<void>) => void;
|
afterEach: (fn: () => MaybePromise<void>) => void;
|
||||||
afterAll: (fn: () => MaybePromise<void>) => void;
|
afterAll: (fn: () => MaybePromise<void>) => void;
|
||||||
|
beforeAll: (fn: () => MaybePromise<void>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function retry<T>(
|
export async function retry<T>(
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ export interface Serializable<Class, Json extends object = object> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MaybePromise<T> = T | Promise<T>;
|
export type MaybePromise<T> = T | Promise<T>;
|
||||||
|
|
||||||
|
export type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> };
|
||||||
|
|||||||
@@ -396,6 +396,38 @@ export function getPath(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setPath(object: object, _path: string | (string | number)[], value: any) {
|
||||||
|
let path = _path;
|
||||||
|
// Optional string-path support.
|
||||||
|
// You can remove this `if` block if you don't need it.
|
||||||
|
if (typeof path === "string") {
|
||||||
|
const isQuoted = (str) => str[0] === '"' && str.at(-1) === '"';
|
||||||
|
path = path
|
||||||
|
.split(/[.\[\]]+/)
|
||||||
|
.filter((x) => x)
|
||||||
|
.map((x) => (!Number.isNaN(Number(x)) ? Number(x) : x))
|
||||||
|
.map((x) => (typeof x === "string" && isQuoted(x) ? x.slice(1, -1) : x));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.length === 0) {
|
||||||
|
throw new Error("The path must have at least one entry in it");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [head, ...tail] = path as any;
|
||||||
|
|
||||||
|
if (tail.length === 0) {
|
||||||
|
object[head] = value;
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(head in object)) {
|
||||||
|
object[head] = typeof tail[0] === "number" ? [] : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
setPath(object[head], tail, value);
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
export function objectToJsLiteral(value: object, indent: number = 0, _level: number = 0): string {
|
export function objectToJsLiteral(value: object, indent: number = 0, _level: number = 0): string {
|
||||||
const nl = indent ? "\n" : "";
|
const nl = indent ? "\n" : "";
|
||||||
const pad = (lvl: number) => (indent ? " ".repeat(indent * lvl) : "");
|
const pad = (lvl: number) => (indent ? " ".repeat(indent * lvl) : "");
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ export class InvalidSchemaError extends Error {
|
|||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
`Invalid schema given for ${JSON.stringify(value, null, 2)}\n\n` +
|
`Invalid schema given for ${JSON.stringify(value, null, 2)}\n\n` +
|
||||||
`Error: ${JSON.stringify(errors[0], null, 2)}`,
|
`Error: ${JSON.stringify(errors[0], null, 2)}\n\n` +
|
||||||
|
`Schema: ${JSON.stringify(schema.toJSON(), null, 2)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const _oldConsoles = {
|
|||||||
warn: console.warn,
|
warn: console.warn,
|
||||||
error: console.error,
|
error: console.error,
|
||||||
};
|
};
|
||||||
|
let _oldStderr: any;
|
||||||
|
let _oldStdout: any;
|
||||||
|
|
||||||
export async function withDisabledConsole<R>(
|
export async function withDisabledConsole<R>(
|
||||||
fn: () => Promise<R>,
|
fn: () => Promise<R>,
|
||||||
@@ -36,10 +38,17 @@ export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"
|
|||||||
severities.forEach((severity) => {
|
severities.forEach((severity) => {
|
||||||
console[severity] = () => null;
|
console[severity] = () => null;
|
||||||
});
|
});
|
||||||
|
// Disable stderr
|
||||||
|
_oldStderr = process.stderr.write;
|
||||||
|
_oldStdout = process.stdout.write;
|
||||||
|
process.stderr.write = () => true;
|
||||||
|
process.stdout.write = () => true;
|
||||||
$console?.setLevel("critical");
|
$console?.setLevel("critical");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableConsoleLog() {
|
export function enableConsoleLog() {
|
||||||
|
process.stderr.write = _oldStderr;
|
||||||
|
process.stdout.write = _oldStdout;
|
||||||
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||||
console[severity as ConsoleSeverity] = fn;
|
console[severity as ConsoleSeverity] = fn;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
|||||||
) {
|
) {
|
||||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||||
type T = RepositoryResultJSON<Data>;
|
type T = RepositoryResultJSON<Data>;
|
||||||
|
|
||||||
|
// @todo: if none found, still returns meta...
|
||||||
|
|
||||||
return this.readMany(entity, {
|
return this.readMany(entity, {
|
||||||
...query,
|
...query,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export class DataController extends Controller {
|
|||||||
"/sync",
|
"/sync",
|
||||||
permission(DataPermissions.databaseSync),
|
permission(DataPermissions.databaseSync),
|
||||||
mcpTool("data_sync", {
|
mcpTool("data_sync", {
|
||||||
|
// @todo: should be removed if readonly
|
||||||
annotations: {
|
annotations: {
|
||||||
destructiveHint: true,
|
destructiveHint: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { getPath } from "bknd/utils";
|
|||||||
import * as proto from "data/prototype";
|
import * as proto from "data/prototype";
|
||||||
import { createApp } from "App";
|
import { createApp } from "App";
|
||||||
import type { MaybePromise } from "core/types";
|
import type { MaybePromise } from "core/types";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
// @todo: add various datatypes: string, number, boolean, object, array, null, undefined, date, etc.
|
// @todo: add various datatypes: string, number, boolean, object, array, null, undefined, date, etc.
|
||||||
// @todo: add toDriver/fromDriver tests on all types and fields
|
// @todo: add toDriver/fromDriver tests on all types and fields
|
||||||
@@ -21,7 +22,9 @@ export function connectionTestSuite(
|
|||||||
rawDialectDetails: string[];
|
rawDialectDetails: string[];
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { test, expect, describe, beforeEach, afterEach, afterAll } = testRunner;
|
const { test, expect, describe, beforeEach, afterEach, afterAll, beforeAll } = testRunner;
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(() => enableConsoleLog());
|
||||||
|
|
||||||
describe("base", () => {
|
describe("base", () => {
|
||||||
let ctx: Awaited<ReturnType<typeof makeConnection>>;
|
let ctx: Awaited<ReturnType<typeof makeConnection>>;
|
||||||
@@ -247,7 +250,7 @@ export function connectionTestSuite(
|
|||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
connection: ctx.connection,
|
connection: ctx.connection,
|
||||||
initialConfig: {
|
config: {
|
||||||
data: schema.toJSON(),
|
data: schema.toJSON(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -333,7 +336,7 @@ export function connectionTestSuite(
|
|||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
connection: ctx.connection,
|
connection: ctx.connection,
|
||||||
initialConfig: {
|
config: {
|
||||||
data: schema.toJSON(),
|
data: schema.toJSON(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { KyselyPlugin, QueryResult } from "kysely";
|
import type { KyselyPlugin, QueryResult } from "kysely";
|
||||||
import {
|
import {
|
||||||
type IGenericSqlite,
|
type IGenericSqlite,
|
||||||
type OnCreateConnection,
|
|
||||||
type Promisable,
|
type Promisable,
|
||||||
parseBigInt,
|
parseBigInt,
|
||||||
buildQueryFn,
|
buildQueryFn,
|
||||||
@@ -9,6 +8,7 @@ import {
|
|||||||
} from "kysely-generic-sqlite";
|
} from "kysely-generic-sqlite";
|
||||||
import { SqliteConnection } from "./SqliteConnection";
|
import { SqliteConnection } from "./SqliteConnection";
|
||||||
import type { ConnQuery, ConnQueryResults, Features } from "../Connection";
|
import type { ConnQuery, ConnQueryResults, Features } from "../Connection";
|
||||||
|
import type { MaybePromise } from "bknd";
|
||||||
|
|
||||||
export type { IGenericSqlite };
|
export type { IGenericSqlite };
|
||||||
export type TStatement = { sql: string; parameters?: any[] | readonly any[] };
|
export type TStatement = { sql: string; parameters?: any[] | readonly any[] };
|
||||||
@@ -16,11 +16,11 @@ export interface IGenericCustomSqlite<DB = unknown> extends IGenericSqlite<DB> {
|
|||||||
batch?: (stmts: TStatement[]) => Promisable<QueryResult<any>[]>;
|
batch?: (stmts: TStatement[]) => Promisable<QueryResult<any>[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GenericSqliteConnectionConfig = {
|
export type GenericSqliteConnectionConfig<Database = unknown> = {
|
||||||
name?: string;
|
name?: string;
|
||||||
additionalPlugins?: KyselyPlugin[];
|
additionalPlugins?: KyselyPlugin[];
|
||||||
excludeTables?: string[];
|
excludeTables?: string[];
|
||||||
onCreateConnection?: OnCreateConnection;
|
onCreateConnection?: (db: Database) => MaybePromise<void>;
|
||||||
supports?: Partial<Features>;
|
supports?: Partial<Features>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,7 +35,12 @@ export class GenericSqliteConnection<DB = unknown> extends SqliteConnection<DB>
|
|||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
dialect: GenericSqliteDialect,
|
dialect: GenericSqliteDialect,
|
||||||
dialectArgs: [executor, config?.onCreateConnection],
|
dialectArgs: [
|
||||||
|
executor,
|
||||||
|
config?.onCreateConnection && typeof config.onCreateConnection === "function"
|
||||||
|
? (c: any) => config.onCreateConnection?.(c.db.db as any)
|
||||||
|
: undefined,
|
||||||
|
],
|
||||||
additionalPlugins: config?.additionalPlugins,
|
additionalPlugins: config?.additionalPlugins,
|
||||||
excludeTables: config?.excludeTables,
|
excludeTables: config?.excludeTables,
|
||||||
});
|
});
|
||||||
@@ -61,7 +66,6 @@ export class GenericSqliteConnection<DB = unknown> extends SqliteConnection<DB>
|
|||||||
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
|
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
|
||||||
const executor = await this.getExecutor();
|
const executor = await this.getExecutor();
|
||||||
if (!executor.batch) {
|
if (!executor.batch) {
|
||||||
//$console.debug("Batching is not supported by this database");
|
|
||||||
return super.executeQueries(...qbs);
|
return super.executeQueries(...qbs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const entityConfigSchema = s
|
|||||||
sort_field: s.string({ default: config.data.default_primary_field }),
|
sort_field: s.string({ default: config.data.default_primary_field }),
|
||||||
sort_dir: s.string({ enum: ["asc", "desc"], default: "asc" }),
|
sort_dir: s.string({ enum: ["asc", "desc"], default: "asc" }),
|
||||||
primary_format: s.string({ enum: primaryFieldTypes }),
|
primary_format: s.string({ enum: primaryFieldTypes }),
|
||||||
|
fields_order: s.array(s.string()).optional(),
|
||||||
},
|
},
|
||||||
{ default: {} },
|
{ default: {} },
|
||||||
)
|
)
|
||||||
@@ -137,7 +138,9 @@ export class Entity<
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFillableFields(context?: TActionContext, include_virtual?: boolean): Field[] {
|
getFillableFields(context?: TActionContext, include_virtual?: boolean): Field[] {
|
||||||
return this.getFields(include_virtual).filter((field) => field.isFillable(context));
|
return this.getFields({ virtual: include_virtual }).filter((field) =>
|
||||||
|
field.isFillable(context),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequiredFields(): Field[] {
|
getRequiredFields(): Field[] {
|
||||||
@@ -189,9 +192,24 @@ export class Entity<
|
|||||||
return this.fields.findIndex((field) => field.name === name) !== -1;
|
return this.fields.findIndex((field) => field.name === name) !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFields(include_virtual: boolean = false): Field[] {
|
getFields(opts?: { virtual?: boolean; sorted?: boolean }): Field[] {
|
||||||
if (include_virtual) return this.fields;
|
const fields = [...this.fields];
|
||||||
return this.fields.filter((f) => !f.isVirtual());
|
const sort = opts?.sorted !== false;
|
||||||
|
const fields_order = this.config.fields_order;
|
||||||
|
if (fields_order && fields_order.length > 0 && sort) {
|
||||||
|
if (fields_order.length !== this.fields.length) {
|
||||||
|
$console.warn("Fields order must contain all fields");
|
||||||
|
} else {
|
||||||
|
// sort fields by order
|
||||||
|
fields.sort((a, b) => {
|
||||||
|
const a_index = fields_order.indexOf(a.name);
|
||||||
|
const b_index = fields_order.indexOf(b.name);
|
||||||
|
return a_index - b_index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (opts?.virtual) return fields;
|
||||||
|
return fields.filter((f) => !f.isVirtual());
|
||||||
}
|
}
|
||||||
|
|
||||||
addField(field: Field) {
|
addField(field: Field) {
|
||||||
@@ -275,7 +293,7 @@ export class Entity<
|
|||||||
fields = this.getFillableFields(options.context);
|
fields = this.getFillableFields(options.context);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
fields = this.getFields(true);
|
fields = this.getFields({ virtual: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
|
const _fields = Object.fromEntries(fields.map((field) => [field.name, field]));
|
||||||
@@ -309,7 +327,12 @@ export class Entity<
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
type: this.type,
|
type: this.type,
|
||||||
fields: Object.fromEntries(this.fields.map((field) => [field.name, field.toJSON()])),
|
fields: Object.fromEntries(
|
||||||
|
this.getFields({ virtual: true, sorted: true }).map((field) => [
|
||||||
|
field.name,
|
||||||
|
field.toJSON(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
config: this.config,
|
config: this.config,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export class EntityTypescript {
|
|||||||
indentWidth: 2,
|
indentWidth: 2,
|
||||||
indentChar: " ",
|
indentChar: " ",
|
||||||
entityCommentMultiline: true,
|
entityCommentMultiline: true,
|
||||||
fieldCommentMultiline: false,
|
fieldCommentMultiline: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ export class EntityTypescript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
typeName(name: string) {
|
typeName(name: string) {
|
||||||
return autoFormatString(name);
|
return autoFormatString(name).replace(/ /g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldTypesToString(type: TEntityTSType, opts?: { ignore_fields?: string[]; indent?: number }) {
|
fieldTypesToString(type: TEntityTSType, opts?: { ignore_fields?: string[]; indent?: number }) {
|
||||||
|
|||||||
@@ -72,12 +72,12 @@ export class Result<T = unknown> {
|
|||||||
return this.first().parameters;
|
return this.first().parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
get data() {
|
get data(): T {
|
||||||
if (this.options.single) {
|
if (this.options.single) {
|
||||||
return this.first().data?.[0];
|
return this.first().data?.[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.first().data ?? [];
|
return this.first().data ?? ([] as T);
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(qb: Compilable | Compilable[]) {
|
async execute(qb: Compilable | Compilable[]) {
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ export * from "./mutation/Mutator";
|
|||||||
export * from "./query/Repository";
|
export * from "./query/Repository";
|
||||||
export * from "./query/WhereBuilder";
|
export * from "./query/WhereBuilder";
|
||||||
export * from "./query/WithBuilder";
|
export * from "./query/WithBuilder";
|
||||||
|
export * from "./Result";
|
||||||
export * from "./query/RepositoryResult";
|
export * from "./query/RepositoryResult";
|
||||||
export * from "./mutation/MutatorResult";
|
export * from "./mutation/MutatorResult";
|
||||||
|
|||||||
@@ -15,17 +15,6 @@ export class EntityIndex {
|
|||||||
throw new Error("All fields must be instances of Field");
|
throw new Error("All fields must be instances of Field");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unique) {
|
|
||||||
const firstRequired = fields[0]?.isRequired();
|
|
||||||
if (!firstRequired) {
|
|
||||||
throw new Error(
|
|
||||||
`Unique indices must have first field as required: ${fields
|
|
||||||
.map((f) => f.name)
|
|
||||||
.join(", ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
this.name = [
|
this.name = [
|
||||||
unique ? "idx_unique" : "idx",
|
unique ? "idx_unique" : "idx",
|
||||||
|
|||||||
@@ -180,8 +180,15 @@ export class ManyToManyRelation extends EntityRelation<typeof ManyToManyRelation
|
|||||||
|
|
||||||
override getName(): string {
|
override getName(): string {
|
||||||
return [
|
return [
|
||||||
super.getName(),
|
...Array.from(
|
||||||
[this.connectionEntity.name, this.connectionTableMappedName].filter(Boolean),
|
new Set(
|
||||||
|
[
|
||||||
|
this.type().replace(":", ""),
|
||||||
|
this.connectionEntity.name,
|
||||||
|
this.connectionTableMappedName,
|
||||||
|
].filter(Boolean),
|
||||||
|
),
|
||||||
|
),
|
||||||
].join("_");
|
].join("_");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./
|
|||||||
import { EntityRelationAnchor } from "./EntityRelationAnchor";
|
import { EntityRelationAnchor } from "./EntityRelationAnchor";
|
||||||
import { type RelationType, RelationTypes } from "./relation-types";
|
import { type RelationType, RelationTypes } from "./relation-types";
|
||||||
import { s } from "bknd/utils";
|
import { s } from "bknd/utils";
|
||||||
|
import type { PrimaryFieldType } from "bknd";
|
||||||
|
|
||||||
export type PolymorphicRelationConfig = s.Static<typeof PolymorphicRelation.schema>;
|
export type PolymorphicRelationConfig = s.Static<typeof PolymorphicRelation.schema>;
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ export class PolymorphicRelation extends EntityRelation<typeof PolymorphicRelati
|
|||||||
.groupBy(groupBy);
|
.groupBy(groupBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
override getReferenceQuery(entity: Entity, id: number): Partial<RepoQuery> {
|
override getReferenceQuery(entity: Entity, id: PrimaryFieldType): Partial<RepoQuery> {
|
||||||
const info = this.queryInfo(entity);
|
const info = this.queryInfo(entity);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export class SchemaManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getIntrospectionFromEntity(entity: Entity): IntrospectedTable {
|
getIntrospectionFromEntity(entity: Entity): IntrospectedTable {
|
||||||
const fields = entity.getFields(false);
|
const fields = entity.getFields({ virtual: false, sorted: true });
|
||||||
const indices = this.em.getIndicesOf(entity);
|
const indices = this.em.getIndicesOf(entity);
|
||||||
|
|
||||||
// this is intentionally setting values to defaults, like "nullable" and "default"
|
// this is intentionally setting values to defaults, like "nullable" and "default"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export {
|
|||||||
type InitialModuleConfigs,
|
type InitialModuleConfigs,
|
||||||
ModuleManagerEvents,
|
ModuleManagerEvents,
|
||||||
} from "./modules/ModuleManager";
|
} from "./modules/ModuleManager";
|
||||||
|
export type * from "modules/ModuleApi";
|
||||||
|
|
||||||
export type { ServerEnv } from "modules/Controller";
|
export type { ServerEnv } from "modules/Controller";
|
||||||
export type { BkndConfig } from "bknd/adapter";
|
export type { BkndConfig } from "bknd/adapter";
|
||||||
@@ -115,6 +116,7 @@ export {
|
|||||||
genericSqlite,
|
genericSqlite,
|
||||||
genericSqliteUtils,
|
genericSqliteUtils,
|
||||||
type GenericSqliteConnection,
|
type GenericSqliteConnection,
|
||||||
|
type GenericSqliteConnectionConfig,
|
||||||
} from "data/connection/sqlite/GenericSqliteConnection";
|
} from "data/connection/sqlite/GenericSqliteConnection";
|
||||||
export {
|
export {
|
||||||
EntityTypescript,
|
EntityTypescript,
|
||||||
@@ -128,6 +130,7 @@ export type { EntityRelation } from "data/relations";
|
|||||||
export type * from "data/entities/Entity";
|
export type * from "data/entities/Entity";
|
||||||
export type { EntityManager } from "data/entities/EntityManager";
|
export type { EntityManager } from "data/entities/EntityManager";
|
||||||
export type { SchemaManager } from "data/schema/SchemaManager";
|
export type { SchemaManager } from "data/schema/SchemaManager";
|
||||||
|
export type * from "data/entities";
|
||||||
export {
|
export {
|
||||||
BaseIntrospector,
|
BaseIntrospector,
|
||||||
Connection,
|
Connection,
|
||||||
|
|||||||
@@ -181,16 +181,14 @@ export class MediaController extends Controller {
|
|||||||
"param",
|
"param",
|
||||||
s.object({
|
s.object({
|
||||||
entity: entitiesEnum,
|
entity: entitiesEnum,
|
||||||
id: s.number(),
|
id: s.anyOf([s.number(), s.string()]),
|
||||||
field: s.string(),
|
field: s.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
jsc("query", s.object({ overwrite: s.boolean().optional() })),
|
jsc("query", s.object({ overwrite: s.boolean().optional() })),
|
||||||
permission([DataPermissions.entityCreate, MediaPermissions.uploadFile]),
|
permission([DataPermissions.entityCreate, MediaPermissions.uploadFile]),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const entity_name = c.req.param("entity");
|
const { entity: entity_name, id: entity_id, field: field_name } = c.req.valid("param");
|
||||||
const field_name = c.req.param("field");
|
|
||||||
const entity_id = Number.parseInt(c.req.param("id"));
|
|
||||||
|
|
||||||
// check if entity exists
|
// check if entity exists
|
||||||
const entity = this.media.em.entity(entity_name);
|
const entity = this.media.em.entity(entity_name);
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ export const mediaFields = {
|
|||||||
etag: text(),
|
etag: text(),
|
||||||
modified_at: datetime(),
|
modified_at: datetime(),
|
||||||
reference: text(),
|
reference: text(),
|
||||||
entity_id: number(),
|
entity_id: text(),
|
||||||
metadata: json(),
|
metadata: json(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class Storage implements EmitsEvents {
|
|||||||
...dim,
|
...dim,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$console.warn("Failed to get image dimensions", e);
|
$console.warn("Failed to get image dimensions", e, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { hash, pickHeaders, s, parse } from "bknd/utils";
|
import { hash, pickHeaders, s, parse, secret } from "bknd/utils";
|
||||||
import type { FileBody, FileListObject, FileMeta } from "../../Storage";
|
import type { FileBody, FileListObject, FileMeta } from "../../Storage";
|
||||||
import { StorageAdapter } from "../../StorageAdapter";
|
import { StorageAdapter } from "../../StorageAdapter";
|
||||||
|
|
||||||
export const cloudinaryAdapterConfig = s.object(
|
export const cloudinaryAdapterConfig = s.object(
|
||||||
{
|
{
|
||||||
cloud_name: s.string(),
|
cloud_name: s.string(),
|
||||||
api_key: s.string(),
|
api_key: secret(),
|
||||||
api_secret: s.string(),
|
api_secret: secret(),
|
||||||
upload_preset: s.string().optional(),
|
upload_preset: s.string().optional(),
|
||||||
},
|
},
|
||||||
{ title: "Cloudinary", description: "Cloudinary media storage" },
|
{ title: "Cloudinary", description: "Cloudinary media storage" },
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ import type {
|
|||||||
} from "@aws-sdk/client-s3";
|
} from "@aws-sdk/client-s3";
|
||||||
import { AwsClient } from "core/clients/aws/AwsClient";
|
import { AwsClient } from "core/clients/aws/AwsClient";
|
||||||
import { isDebug } from "core/env";
|
import { isDebug } from "core/env";
|
||||||
import { isFile, pickHeaders2, parse, s } from "bknd/utils";
|
import { isFile, pickHeaders2, parse, s, secret } from "bknd/utils";
|
||||||
import { transform } from "lodash-es";
|
import { transform } from "lodash-es";
|
||||||
import type { FileBody, FileListObject } from "../../Storage";
|
import type { FileBody, FileListObject } from "../../Storage";
|
||||||
import { StorageAdapter } from "../../StorageAdapter";
|
import { StorageAdapter } from "../../StorageAdapter";
|
||||||
|
|
||||||
export const s3AdapterConfig = s.object(
|
export const s3AdapterConfig = s.object(
|
||||||
{
|
{
|
||||||
access_key: s.string(),
|
access_key: secret(),
|
||||||
secret_access_key: s.string(),
|
secret_access_key: secret(),
|
||||||
url: s.string({
|
url: s.string({
|
||||||
pattern: "^https?://(?:.*)?[^/.]+$",
|
pattern: "^https?://(?:.*)?[^/.]+$",
|
||||||
description: "URL to S3 compatible endpoint without trailing slash",
|
description: "URL to S3 compatible endpoint without trailing slash",
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
import { mark, stripMark, $console, s, objectEach, transformObject, McpServer } from "bknd/utils";
|
import { objectEach, transformObject, McpServer, type s, SecretSchema, setPath } 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";
|
||||||
import { env } from "core/env";
|
import { env } from "core/env";
|
||||||
import { BkndError } from "core/errors";
|
|
||||||
import { EventManager, Event } from "core/events";
|
import { EventManager, Event } from "core/events";
|
||||||
import * as $diff from "core/object/diff";
|
|
||||||
import type { Connection } from "data/connection";
|
import type { Connection } from "data/connection";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
import { EntityManager } from "data/entities/EntityManager";
|
||||||
import * as proto from "data/prototype";
|
|
||||||
import { TransformPersistFailedException } from "data/errors";
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import type { Kysely } from "kysely";
|
|
||||||
import { mergeWith } from "lodash-es";
|
|
||||||
import { CURRENT_VERSION, TABLE_NAME, migrate } from "modules/migrations";
|
|
||||||
import { AppServer } from "modules/server/AppServer";
|
|
||||||
import { AppAuth } from "../auth/AppAuth";
|
|
||||||
import { AppData } from "../data/AppData";
|
|
||||||
import { AppFlows } from "../flows/AppFlows";
|
|
||||||
import { AppMedia } from "../media/AppMedia";
|
|
||||||
import type { ServerEnv } from "./Controller";
|
import type { ServerEnv } from "./Controller";
|
||||||
import { Module, type ModuleBuildContext } from "./Module";
|
import { Module, type ModuleBuildContext } from "./Module";
|
||||||
import { ModuleHelper } from "./ModuleHelper";
|
import { ModuleHelper } from "./ModuleHelper";
|
||||||
|
import { AppServer } from "modules/server/AppServer";
|
||||||
|
import { AppAuth } from "auth/AppAuth";
|
||||||
|
import { AppData } from "data/AppData";
|
||||||
|
import { AppFlows } from "flows/AppFlows";
|
||||||
|
import { AppMedia } from "media/AppMedia";
|
||||||
|
import type { PartialRec } from "core/types";
|
||||||
|
import { mergeWith, pick } from "lodash-es";
|
||||||
|
|
||||||
export type { ModuleBuildContext };
|
export type { ModuleBuildContext };
|
||||||
|
|
||||||
@@ -47,13 +42,8 @@ export type ModuleSchemas = {
|
|||||||
export type ModuleConfigs = {
|
export type ModuleConfigs = {
|
||||||
[K in keyof ModuleSchemas]: s.Static<ModuleSchemas[K]>;
|
[K in keyof ModuleSchemas]: s.Static<ModuleSchemas[K]>;
|
||||||
};
|
};
|
||||||
type PartialRec<T> = { [P in keyof T]?: PartialRec<T[P]> };
|
|
||||||
|
|
||||||
export type InitialModuleConfigs =
|
export type InitialModuleConfigs = { version?: number } & PartialRec<ModuleConfigs>;
|
||||||
| ({
|
|
||||||
version: number;
|
|
||||||
} & ModuleConfigs)
|
|
||||||
| PartialRec<ModuleConfigs>;
|
|
||||||
|
|
||||||
enum Verbosity {
|
enum Verbosity {
|
||||||
silent = 0,
|
silent = 0,
|
||||||
@@ -80,42 +70,14 @@ export type ModuleManagerOptions = {
|
|||||||
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
seed?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||||
// called right after modules are built, before finish
|
// called right after modules are built, before finish
|
||||||
onModulesBuilt?: (ctx: ModuleBuildContext) => Promise<void>;
|
onModulesBuilt?: (ctx: ModuleBuildContext) => Promise<void>;
|
||||||
|
// whether to store secrets in the database
|
||||||
|
storeSecrets?: boolean;
|
||||||
|
// provided secrets
|
||||||
|
secrets?: Record<string, any>;
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
verbosity?: Verbosity;
|
verbosity?: Verbosity;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigTable<Json = ModuleConfigs> = {
|
|
||||||
id?: number;
|
|
||||||
version: number;
|
|
||||||
type: "config" | "diff" | "backup";
|
|
||||||
json: Json;
|
|
||||||
created_at?: Date;
|
|
||||||
updated_at?: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
const configJsonSchema = s.anyOf([
|
|
||||||
getDefaultSchema(),
|
|
||||||
s.array(
|
|
||||||
s.strictObject({
|
|
||||||
t: s.string({ enum: ["a", "r", "e"] }),
|
|
||||||
p: s.array(s.anyOf([s.string(), s.number()])),
|
|
||||||
o: s.any().optional(),
|
|
||||||
n: s.any().optional(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
export const __bknd = proto.entity(TABLE_NAME, {
|
|
||||||
version: proto.number().required(),
|
|
||||||
type: proto.enumm({ enum: ["config", "diff", "backup"] }).required(),
|
|
||||||
json: proto.jsonSchema({ schema: configJsonSchema.toJSON() }).required(),
|
|
||||||
created_at: proto.datetime(),
|
|
||||||
updated_at: proto.datetime(),
|
|
||||||
});
|
|
||||||
type ConfigTable2 = proto.Schema<typeof __bknd>;
|
|
||||||
interface T_INTERNAL_EM {
|
|
||||||
__bknd: ConfigTable2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const debug_modules = env("modules_debug");
|
const debug_modules = env("modules_debug");
|
||||||
|
|
||||||
abstract class ModuleManagerEvent<A = {}> extends Event<{ ctx: ModuleBuildContext } & A> {}
|
abstract class ModuleManagerEvent<A = {}> extends Event<{ ctx: ModuleBuildContext } & A> {}
|
||||||
@@ -127,8 +89,14 @@ export class ModuleManagerConfigUpdateEvent<
|
|||||||
}> {
|
}> {
|
||||||
static override slug = "mm-config-update";
|
static override slug = "mm-config-update";
|
||||||
}
|
}
|
||||||
|
export class ModuleManagerSecretsExtractedEvent extends ModuleManagerEvent<{
|
||||||
|
secrets: Record<string, any>;
|
||||||
|
}> {
|
||||||
|
static override slug = "mm-secrets-extracted";
|
||||||
|
}
|
||||||
export const ModuleManagerEvents = {
|
export const ModuleManagerEvents = {
|
||||||
ModuleManagerConfigUpdateEvent,
|
ModuleManagerConfigUpdateEvent,
|
||||||
|
ModuleManagerSecretsExtractedEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
// @todo: cleanup old diffs on upgrade
|
// @todo: cleanup old diffs on upgrade
|
||||||
@@ -137,8 +105,6 @@ export class ModuleManager {
|
|||||||
static Events = ModuleManagerEvents;
|
static Events = ModuleManagerEvents;
|
||||||
|
|
||||||
protected modules: Modules;
|
protected modules: Modules;
|
||||||
// internal em for __bknd config table
|
|
||||||
__em!: EntityManager<T_INTERNAL_EM>;
|
|
||||||
// ctx for modules
|
// ctx for modules
|
||||||
em!: EntityManager;
|
em!: EntityManager;
|
||||||
server!: Hono<ServerEnv>;
|
server!: Hono<ServerEnv>;
|
||||||
@@ -146,42 +112,24 @@ export class ModuleManager {
|
|||||||
guard!: Guard;
|
guard!: Guard;
|
||||||
mcp!: ModuleBuildContext["mcp"];
|
mcp!: ModuleBuildContext["mcp"];
|
||||||
|
|
||||||
private _version: number = 0;
|
protected _built = false;
|
||||||
private _built = false;
|
|
||||||
private readonly _booted_with?: "provided" | "partial";
|
|
||||||
private _stable_configs: ModuleConfigs | undefined;
|
|
||||||
|
|
||||||
private logger: DebugLogger;
|
protected logger: DebugLogger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly connection: Connection,
|
protected readonly connection: Connection,
|
||||||
private options?: Partial<ModuleManagerOptions>,
|
protected options?: Partial<ModuleManagerOptions>,
|
||||||
) {
|
) {
|
||||||
this.__em = new EntityManager([__bknd], this.connection);
|
|
||||||
this.modules = {} as Modules;
|
this.modules = {} as Modules;
|
||||||
this.emgr = new EventManager({ ...ModuleManagerEvents });
|
this.emgr = new EventManager({ ...ModuleManagerEvents });
|
||||||
this.logger = new DebugLogger(debug_modules);
|
this.logger = new DebugLogger(debug_modules);
|
||||||
let initial = {} as Partial<ModuleConfigs>;
|
|
||||||
|
|
||||||
if (options?.initial) {
|
this.createModules(options?.initial ?? {});
|
||||||
if ("version" in options.initial) {
|
|
||||||
const { version, ...initialConfig } = options.initial;
|
|
||||||
this._version = version;
|
|
||||||
initial = stripMark(initialConfig);
|
|
||||||
|
|
||||||
this._booted_with = "provided";
|
|
||||||
} else {
|
|
||||||
initial = mergeWith(getDefaultConfig(), options.initial);
|
|
||||||
this._booted_with = "partial";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log("booted with", this._booted_with);
|
|
||||||
|
|
||||||
this.createModules(initial);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createModules(initial: Partial<ModuleConfigs>) {
|
protected onModuleConfigUpdated(key: string, config: any) {}
|
||||||
|
|
||||||
|
private createModules(initial: PartialRec<ModuleConfigs>) {
|
||||||
this.logger.context("createModules").log("creating modules");
|
this.logger.context("createModules").log("creating modules");
|
||||||
try {
|
try {
|
||||||
const context = this.ctx(true);
|
const context = this.ctx(true);
|
||||||
@@ -211,46 +159,7 @@ export class ModuleManager {
|
|||||||
return this._built;
|
return this._built;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected rebuildServer() {
|
||||||
* This is set through module's setListener
|
|
||||||
* It's called everytime a module's config is updated in SchemaObject
|
|
||||||
* Needs to rebuild modules and save to database
|
|
||||||
*/
|
|
||||||
private async onModuleConfigUpdated(key: string, config: any) {
|
|
||||||
if (this.options?.onUpdated) {
|
|
||||||
await this.options.onUpdated(key as any, config);
|
|
||||||
} else {
|
|
||||||
await this.buildModules();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private repo() {
|
|
||||||
return this.__em.repo(__bknd, {
|
|
||||||
// to prevent exceptions when table doesn't exist
|
|
||||||
silent: true,
|
|
||||||
// disable counts for performance and compatibility
|
|
||||||
includeCounts: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private mutator() {
|
|
||||||
return this.__em.mutator(__bknd);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get db() {
|
|
||||||
// @todo: check why this is neccessary
|
|
||||||
return this.connection.kysely as unknown as Kysely<{ table: ConfigTable }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @todo: add indices for: version, type
|
|
||||||
async syncConfigTable() {
|
|
||||||
this.logger.context("sync").log("start");
|
|
||||||
const result = await this.__em.schema().sync({ force: true });
|
|
||||||
this.logger.log("done").clear();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private rebuildServer() {
|
|
||||||
this.server = new Hono<ServerEnv>();
|
this.server = new Hono<ServerEnv>();
|
||||||
if (this.options?.basePath) {
|
if (this.options?.basePath) {
|
||||||
this.server = this.server.basePath(this.options.basePath);
|
this.server = this.server.basePath(this.options.basePath);
|
||||||
@@ -299,252 +208,81 @@ export class ModuleManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetch(): Promise<ConfigTable | undefined> {
|
extractSecrets() {
|
||||||
this.logger.context("fetch").log("fetching");
|
const moduleConfigs = structuredClone(this.configs());
|
||||||
const startTime = performance.now();
|
const secrets = { ...this.options?.secrets };
|
||||||
|
const extractedKeys: string[] = [];
|
||||||
|
|
||||||
// disabling console log, because the table might not exist yet
|
for (const [key, module] of Object.entries(this.modules)) {
|
||||||
const { data: result } = await this.repo().findOne(
|
const config = moduleConfigs[key];
|
||||||
{ type: "config" },
|
const schema = module.getSchema();
|
||||||
{
|
|
||||||
sort: { by: "version", dir: "desc" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result) {
|
const extracted = [...schema.walk({ data: config })].filter(
|
||||||
this.logger.log("error fetching").clear();
|
(n) => n.schema instanceof SecretSchema,
|
||||||
return undefined;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
this.logger
|
for (const n of extracted) {
|
||||||
.log("took", performance.now() - startTime, "ms", {
|
const path = [key, ...n.instancePath].join(".");
|
||||||
version: result.version,
|
|
||||||
id: result.id,
|
|
||||||
})
|
|
||||||
.clear();
|
|
||||||
|
|
||||||
return result as unknown as ConfigTable;
|
if (typeof n.data === "string") {
|
||||||
}
|
extractedKeys.push(path);
|
||||||
|
secrets[path] = n.data;
|
||||||
async save() {
|
setPath(moduleConfigs, path, "");
|
||||||
this.logger.context("save").log("saving version", this.version());
|
|
||||||
const configs = this.configs();
|
|
||||||
const version = this.version();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = await this.fetch();
|
|
||||||
if (!state) throw new BkndError("no config found");
|
|
||||||
this.logger.log("fetched version", state.version);
|
|
||||||
|
|
||||||
if (state.version !== version) {
|
|
||||||
// @todo: mark all others as "backup"
|
|
||||||
this.logger.log("version conflict, storing new version", state.version, version);
|
|
||||||
await this.mutator().insertOne({
|
|
||||||
version: state.version,
|
|
||||||
type: "backup",
|
|
||||||
json: configs,
|
|
||||||
});
|
|
||||||
await this.mutator().insertOne({
|
|
||||||
version: version,
|
|
||||||
type: "config",
|
|
||||||
json: configs,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.logger.log("version matches", state.version);
|
|
||||||
|
|
||||||
// clean configs because of Diff() function
|
|
||||||
const diffs = $diff.diff(state.json, $diff.clone(configs));
|
|
||||||
this.logger.log("checking diff", [diffs.length]);
|
|
||||||
|
|
||||||
if (diffs.length > 0) {
|
|
||||||
// validate diffs, it'll throw on invalid
|
|
||||||
this.validateDiffs(diffs);
|
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
// store diff
|
|
||||||
await this.mutator().insertOne({
|
|
||||||
version,
|
|
||||||
type: "diff",
|
|
||||||
json: $diff.clone(diffs),
|
|
||||||
created_at: date,
|
|
||||||
updated_at: date,
|
|
||||||
});
|
|
||||||
|
|
||||||
// store new version
|
|
||||||
await this.mutator().updateWhere(
|
|
||||||
{
|
|
||||||
version,
|
|
||||||
json: configs,
|
|
||||||
updated_at: date,
|
|
||||||
} as any,
|
|
||||||
{
|
|
||||||
type: "config",
|
|
||||||
version,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.logger.log("no diff, not saving");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof BkndError && e.message === "no config found") {
|
|
||||||
this.logger.log("no config, just save fresh");
|
|
||||||
// no config, just save
|
|
||||||
await this.mutator().insertOne({
|
|
||||||
type: "config",
|
|
||||||
version,
|
|
||||||
json: configs,
|
|
||||||
created_at: new Date(),
|
|
||||||
updated_at: new Date(),
|
|
||||||
});
|
|
||||||
} else if (e instanceof TransformPersistFailedException) {
|
|
||||||
$console.error("ModuleManager: Cannot save invalid config");
|
|
||||||
this.revertModules();
|
|
||||||
throw e;
|
|
||||||
} else {
|
|
||||||
$console.error("ModuleManager: Aborting");
|
|
||||||
this.revertModules();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-apply configs to all modules (important for system entities)
|
return {
|
||||||
this.setConfigs(configs);
|
configs: moduleConfigs,
|
||||||
|
secrets: pick(secrets, extractedKeys),
|
||||||
// @todo: cleanup old versions?
|
extractedKeys,
|
||||||
|
};
|
||||||
this.logger.clear();
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private revertModules() {
|
protected async setConfigs(configs: ModuleConfigs): Promise<void> {
|
||||||
if (this._stable_configs) {
|
|
||||||
$console.warn("ModuleManager: Reverting modules");
|
|
||||||
this.setConfigs(this._stable_configs as any);
|
|
||||||
} else {
|
|
||||||
$console.error("ModuleManager: No stable configs to revert to");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates received diffs for an additional security control.
|
|
||||||
* Checks:
|
|
||||||
* - check if module is registered
|
|
||||||
* - run modules onBeforeUpdate() for added protection
|
|
||||||
*
|
|
||||||
* **Important**: Throw `Error` so it won't get catched.
|
|
||||||
*
|
|
||||||
* @param diffs
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private validateDiffs(diffs: $diff.DiffEntry[]): void {
|
|
||||||
// check top level paths, and only allow a single module to be modified in a single transaction
|
|
||||||
const modules = [...new Set(diffs.map((d) => d.p[0]))];
|
|
||||||
if (modules.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const moduleName of modules) {
|
|
||||||
const name = moduleName as ModuleKey;
|
|
||||||
const module = this.get(name) as Module;
|
|
||||||
if (!module) {
|
|
||||||
const msg = "validateDiffs: module not registered";
|
|
||||||
// biome-ignore format: ...
|
|
||||||
$console.error(msg, JSON.stringify({ module: name, diffs }, null, 2));
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// pass diffs to the module to allow it to throw
|
|
||||||
if (this._stable_configs?.[name]) {
|
|
||||||
const current = $diff.clone(this._stable_configs?.[name]);
|
|
||||||
const modified = $diff.apply({ [name]: current }, diffs)[name];
|
|
||||||
module.onBeforeUpdate(current, modified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setConfigs(configs: ModuleConfigs): void {
|
|
||||||
this.logger.log("setting configs");
|
this.logger.log("setting configs");
|
||||||
objectEach(configs, (config, key) => {
|
for await (const [key, config] of Object.entries(configs)) {
|
||||||
|
if (!(key in this.modules)) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// setting "noEmit" to true, to not force listeners to update
|
// setting "noEmit" to true, to not force listeners to update
|
||||||
this.modules[key].schema().set(config as any, true);
|
const result = await this.modules[key].schema().set(config as any, true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to set config for module ${key}: ${JSON.stringify(config, null, 2)}`,
|
`Failed to set config for module ${key}: ${JSON.stringify(config, null, 2)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(opts?: { fetch?: boolean }) {
|
async build(opts?: any) {
|
||||||
this.logger.context("build").log("version", this.version());
|
this.createModules(this.options?.initial ?? {});
|
||||||
await this.ctx().connection.init();
|
await this.buildModules();
|
||||||
|
|
||||||
// if no config provided, try fetch from db
|
// if secrets were provided, extract, merge and build again
|
||||||
if (this.version() === 0 || opts?.fetch === true) {
|
const provided_secrets = this.options?.secrets ?? {};
|
||||||
if (opts?.fetch) {
|
if (Object.keys(provided_secrets).length > 0) {
|
||||||
this.logger.log("force fetch");
|
const { configs, extractedKeys } = this.extractSecrets();
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.fetch();
|
for (const key of extractedKeys) {
|
||||||
|
if (key in provided_secrets) {
|
||||||
// if no version, and nothing found, go with initial
|
setPath(configs, key, provided_secrets[key]);
|
||||||
if (!result) {
|
|
||||||
this.logger.log("nothing in database, go initial");
|
|
||||||
await this.setupInitial();
|
|
||||||
} else {
|
|
||||||
this.logger.log("db has", result.version);
|
|
||||||
// set version and config from fetched
|
|
||||||
this._version = result.version;
|
|
||||||
|
|
||||||
if (this.options?.trustFetched === true) {
|
|
||||||
this.logger.log("trusting fetched config (mark)");
|
|
||||||
mark(result.json);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if version doesn't match, migrate before building
|
|
||||||
if (this.version() !== CURRENT_VERSION) {
|
|
||||||
this.logger.log("now migrating");
|
|
||||||
|
|
||||||
await this.syncConfigTable();
|
|
||||||
|
|
||||||
const version_before = this.version();
|
|
||||||
const [_version, _configs] = await migrate(version_before, result.json, {
|
|
||||||
db: this.db,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._version = _version;
|
|
||||||
this.ctx().flags.sync_required = true;
|
|
||||||
|
|
||||||
this.logger.log("migrated to", _version);
|
|
||||||
$console.log("Migrated config from", version_before, "to", this.version());
|
|
||||||
|
|
||||||
this.createModules(_configs);
|
|
||||||
await this.buildModules();
|
|
||||||
} else {
|
|
||||||
this.logger.log("version is current", this.version());
|
|
||||||
this.createModules(result.json);
|
|
||||||
await this.buildModules();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (this.version() !== CURRENT_VERSION) {
|
await this.setConfigs(configs);
|
||||||
throw new Error(
|
|
||||||
`Given version (${this.version()}) and current version (${CURRENT_VERSION}) do not match.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.logger.log("current version is up to date", this.version());
|
|
||||||
await this.buildModules();
|
await this.buildModules();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log("done");
|
|
||||||
this.logger.clear();
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildModules(options?: { graceful?: boolean; ignoreFlags?: boolean }) {
|
protected async buildModules(options?: {
|
||||||
|
graceful?: boolean;
|
||||||
|
ignoreFlags?: boolean;
|
||||||
|
drop?: boolean;
|
||||||
|
}) {
|
||||||
const state = {
|
const state = {
|
||||||
built: false,
|
built: false,
|
||||||
modules: [] as ModuleKey[],
|
modules: [] as ModuleKey[],
|
||||||
@@ -580,12 +318,8 @@ export class ModuleManager {
|
|||||||
this.logger.log("db sync requested");
|
this.logger.log("db sync requested");
|
||||||
|
|
||||||
// sync db
|
// sync db
|
||||||
await ctx.em.schema().sync({ force: true });
|
await ctx.em.schema().sync({ force: true, drop: options?.drop });
|
||||||
state.synced = true;
|
state.synced = true;
|
||||||
|
|
||||||
// save
|
|
||||||
await this.save();
|
|
||||||
state.saved = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.flags.ctx_reload_required) {
|
if (ctx.flags.ctx_reload_required) {
|
||||||
@@ -601,92 +335,12 @@ export class ModuleManager {
|
|||||||
ctx.flags = Module.ctx_flags;
|
ctx.flags = Module.ctx_flags;
|
||||||
|
|
||||||
// storing last stable config version
|
// storing last stable config version
|
||||||
this._stable_configs = $diff.clone(this.configs());
|
//this._stable_configs = $diff.clone(this.configs());
|
||||||
|
|
||||||
this.logger.clear();
|
this.logger.clear();
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async setupInitial() {
|
|
||||||
this.logger.context("initial").log("start");
|
|
||||||
this._version = CURRENT_VERSION;
|
|
||||||
await this.syncConfigTable();
|
|
||||||
const state = await this.buildModules();
|
|
||||||
if (!state.saved) {
|
|
||||||
await this.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = {
|
|
||||||
...this.ctx(),
|
|
||||||
// disable events for initial setup
|
|
||||||
em: this.ctx().em.fork(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// perform a sync
|
|
||||||
await ctx.em.schema().sync({ force: true });
|
|
||||||
await this.options?.seed?.(ctx);
|
|
||||||
|
|
||||||
// run first boot event
|
|
||||||
await this.options?.onFirstBoot?.();
|
|
||||||
this.logger.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateConfigSafe<Module extends keyof Modules>(
|
|
||||||
name: Module,
|
|
||||||
): Pick<ReturnType<Modules[Module]["schema"]>, "set" | "patch" | "overwrite" | "remove"> {
|
|
||||||
const module = this.modules[name];
|
|
||||||
|
|
||||||
return new Proxy(module.schema(), {
|
|
||||||
get: (target, prop: string) => {
|
|
||||||
if (!["set", "patch", "overwrite", "remove"].includes(prop)) {
|
|
||||||
throw new Error(`Method ${prop} is not allowed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return async (...args) => {
|
|
||||||
$console.log("[Safe Mutate]", name);
|
|
||||||
try {
|
|
||||||
// overwrite listener to run build inside this try/catch
|
|
||||||
module.setListener(async () => {
|
|
||||||
await this.emgr.emit(
|
|
||||||
new ModuleManagerConfigUpdateEvent({
|
|
||||||
ctx: this.ctx(),
|
|
||||||
module: name,
|
|
||||||
config: module.config as any,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await this.buildModules();
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await target[prop](...args);
|
|
||||||
|
|
||||||
// revert to original listener
|
|
||||||
module.setListener(async (c) => {
|
|
||||||
await this.onModuleConfigUpdated(name, c);
|
|
||||||
});
|
|
||||||
|
|
||||||
// if there was an onUpdated listener, call it after success
|
|
||||||
// e.g. App uses it to register module routes
|
|
||||||
if (this.options?.onUpdated) {
|
|
||||||
await this.options.onUpdated(name, module.config as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
$console.error(`[Safe Mutate] failed "${name}":`, e);
|
|
||||||
|
|
||||||
// revert to previous config & rebuild using original listener
|
|
||||||
this.revertModules();
|
|
||||||
await this.onModuleConfigUpdated(name, module.config as any);
|
|
||||||
$console.warn(`[Safe Mutate] reverted "${name}":`);
|
|
||||||
|
|
||||||
// make sure to throw the error
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get<K extends keyof Modules>(key: K): Modules[K] {
|
get<K extends keyof Modules>(key: K): Modules[K] {
|
||||||
if (!(key in this.modules)) {
|
if (!(key in this.modules)) {
|
||||||
throw new Error(`Module "${key}" doesn't exist, cannot get`);
|
throw new Error(`Module "${key}" doesn't exist, cannot get`);
|
||||||
@@ -695,7 +349,7 @@ export class ModuleManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
version() {
|
version() {
|
||||||
return this._version;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
built() {
|
built() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user