mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1e2456ada | |||
| bebf9e0411 | |||
| ca55503e66 | |||
| 1ec3ba0e92 | |||
| ea372a8125 | |||
| 847c264b35 | |||
| b4f024bef8 | |||
| 8bc63ba2a7 | |||
| cecb0e829a | |||
| 491bd65da1 | |||
| ba7fea5571 | |||
| ba7587aa3f | |||
| 0a49061549 | |||
| 682ecc6dac | |||
| efbc5f115f | |||
| b4d6b1c04d | |||
| e21f1a3ee2 | |||
| a9efee3555 | |||
| 2ca1f64ae6 | |||
| 2b32a836cd | |||
| 9d5bc61f33 | |||
| ea46dc8df0 | |||
| 88ec3432a0 | |||
| 43c8f14470 | |||
| c384bf4dd4 | |||
| db2a994a01 | |||
| 166ea4a71b | |||
| 0d3bb3b7d6 | |||
| cfbec5b6ea | |||
| 957355c862 | |||
| fc282b795e | |||
| bc65f5f311 | |||
| 5355f2593e | |||
| e939debba5 | |||
| b35ee36fb1 | |||
| 109c72e84f | |||
| 7e0b02a125 | |||
| d4076803c6 | |||
| a5959acb34 | |||
| a4ddf780ce | |||
| 2f684765de | |||
| 5143ee5726 | |||
| 22b54862e8 | |||
| fe1716ed01 | |||
| 0c31dcdb95 | |||
| 4cc0f8e172 | |||
| 45138c25f0 | |||
| 80034b9b0a | |||
| e6ee75c712 | |||
| 56287eb05e | |||
| 144f35ec66 | |||
| d1378c6c51 | |||
| 4e10b36d0d | |||
| 70737c04cd | |||
| d41fd5541f | |||
| e76e3531d7 | |||
| a9f3a582eb | |||
| 57ae2f333c | |||
| c161a26ec0 | |||
| 6e78a4c238 | |||
| 42edce904f | |||
| b2086c4da7 | |||
| 344d729320 | |||
| c86f4c12b7 | |||
| aaa97ed113 | |||
| 69c8aec6fb | |||
| f5ceffd80e | |||
| a9f367aa2c | |||
| b87696a0db | |||
| 3338804c34 | |||
| 6b3ac9e6e2 | |||
| 8e1ecfcfe3 | |||
| 6e2596fb77 | |||
| 2ada4e9f20 | |||
| cc038a0a9a | |||
| af6d1960b9 | |||
| 96ae8f9d63 | |||
| c09d1d53b3 | |||
| 2239333f08 | |||
| 0710b5bf1f | |||
| 03c4c41d2d | |||
| 85d8542a2e | |||
| f8d2a9090e | |||
| bbb7bfb7a1 | |||
| 28e277afe1 | |||
| 8517c9b90b | |||
| fe5ccd4206 | |||
| 8b4b63b3cd | |||
| 9c4aac8843 | |||
| d5bb6ffa61 | |||
| fc513bb413 | |||
| 4162b9878a | |||
| c75f8d0937 | |||
| 046c1d21b1 | |||
| 6c2e579596 | |||
| 88419548c7 | |||
| 12c955155d | |||
| 15af9f5261 | |||
| 39efb911e9 | |||
| 4729203d47 | |||
| 0db052acca | |||
| 6e08f45857 | |||
| 7bff84d601 | |||
| e66e05b2b0 | |||
| 5b318ce485 | |||
| 22de16fe17 | |||
| 5d26673bc6 | |||
| dfa4bc0e33 | |||
| e51b89a18a | |||
| 63988e0c5f | |||
| 297fd85a4f | |||
| a82fbe7400 | |||
| 74e1e9a03f | |||
| 05a81b5bdc | |||
| 9ff49103fb | |||
| a5b59c004e | |||
| a0edcf483b | |||
| 9a18e354cd | |||
| 58c7aba1a4 | |||
| f8aa242d2b | |||
| 3da9570abe | |||
| 6ee898e606 | |||
| abbd372ddf | |||
| 3e77982996 | |||
| 3fbea8ace7 | |||
| a41f943b43 | |||
| 15a9c549e7 | |||
| 50cadbaa8e | |||
| 7b128c9701 | |||
| 061181d59d |
@@ -12,10 +12,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: "1.2.14"
|
||||
bun-version: "1.2.19"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./app
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[](https://npmjs.org/package/bknd)
|
||||
[](https://www.npmjs.com/package/bknd)
|
||||
|
||||

|
||||
|
||||
@@ -10,22 +9,30 @@
|
||||
</p>
|
||||
|
||||
bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
||||
* **Runtimes**: Node.js 22+, Bun 1.0+, Deno, Browser, Cloudflare Workers/Pages, Vercel, Netlify, AWS Lambda, etc.
|
||||
* **Databases**:
|
||||
* SQLite: LibSQL, Node SQLite, Bun SQLite, Cloudflare D1, Cloudflare Durable Objects SQLite, SQLocal
|
||||
* Postgres: Vanilla Postgres, Supabase, Neon, Xata
|
||||
* **Frameworks**: React, Next.js, React Router, Astro, Vite, Waku
|
||||
* **Storage**: AWS S3, S3-compatible (Tigris, R2, Minio, etc.), Cloudflare R2 (binding), Cloudinary, Filesystem
|
||||
|
||||
**For documentation and examples, please visit https://docs.bknd.io.**
|
||||
|
||||
> [!WARNING]
|
||||
> This project requires Node.js 22 or higher (because of `node:sqlite`).
|
||||
>
|
||||
> Please keep in mind that **bknd** is still under active development
|
||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||
|
||||
## Size
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
||||
|
||||
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 212 kB gzipped (e.g. deployed as Cloudflare Worker).
|
||||
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 300 kB gzipped (e.g. deployed as Cloudflare Worker).
|
||||
|
||||
## Motivation
|
||||
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# ===== DB Settings =====
|
||||
VITE_DB_URL=:memory:
|
||||
# you can set a location for a database here, it'll overwrite the previous setting
|
||||
# ideally use the ".db" folder (create it first), it's git ignored
|
||||
VITE_DB_URL=file:.db/dev.db
|
||||
|
||||
# alternatively, you can use url/token combination
|
||||
#VITE_DB_URL=
|
||||
#VITE_DB_TOKEN=
|
||||
|
||||
|
||||
# ===== DEV Server =====
|
||||
# restart the dev server on every change (enable with "1")
|
||||
VITE_APP_FRESH=
|
||||
# displays react-scan widget (enable with "1")
|
||||
VITE_DEBUG_RERENDERS=
|
||||
# console logs registered routes on start (enable with "1")
|
||||
VITE_SHOW_ROUTES=
|
||||
|
||||
|
||||
# ===== Test Credentials =====
|
||||
RESEND_API_KEY=
|
||||
R2_TOKEN=
|
||||
|
||||
R2_ACCESS_KEY=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_URL=
|
||||
|
||||
AWS_ACCESS_KEY=
|
||||
AWS_SECRET_KEY=
|
||||
AWS_S3_URL=
|
||||
|
||||
OAUTH_CLIENT_ID=
|
||||
OAUTH_CLIENT_SECRET=
|
||||
|
||||
PUBLIC_POSTHOG_KEY=
|
||||
PUBLIC_POSTHOG_HOST=
|
||||
|
||||
# ===== Internals =====
|
||||
BKND_CLI_CREATE_REF=main
|
||||
BKND_CLI_LOG_LEVEL=debug
|
||||
BKND_MODULES_DEBUG=1
|
||||
+130
-12
@@ -1,6 +1,9 @@
|
||||
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
||||
import { App } from "../src";
|
||||
import { afterEach, describe, test, expect } from "bun:test";
|
||||
import { App, createApp } from "core/test/utils";
|
||||
import { getDummyConnection } from "./helper";
|
||||
import { Hono } from "hono";
|
||||
import * as proto from "../src/data/prototype";
|
||||
import { pick } from "lodash-es";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterEach(afterAllCleanup);
|
||||
@@ -10,18 +13,133 @@ describe("App tests", async () => {
|
||||
const app = new App(dummyConnection);
|
||||
await app.build();
|
||||
|
||||
//expect(await app.data?.em.ping()).toBeTrue();
|
||||
expect(await app.em.ping()).toBeTrue();
|
||||
});
|
||||
|
||||
/*test.only("what", async () => {
|
||||
const app = new App(dummyConnection, {
|
||||
auth: {
|
||||
enabled: true,
|
||||
test("plugins", async () => {
|
||||
const called: string[] = [];
|
||||
const app = createApp({
|
||||
initialConfig: {
|
||||
auth: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
plugins: [
|
||||
(app) => {
|
||||
expect(app).toBeDefined();
|
||||
expect(app).toBeInstanceOf(App);
|
||||
return {
|
||||
name: "test",
|
||||
schema: () => {
|
||||
called.push("schema");
|
||||
return proto.em(
|
||||
{
|
||||
posts: proto.entity("posts", {
|
||||
title: proto.text(),
|
||||
}),
|
||||
comments: proto.entity("comments", {
|
||||
content: proto.text(),
|
||||
}),
|
||||
users: proto.entity("users", {
|
||||
email_verified: proto.boolean(),
|
||||
}),
|
||||
},
|
||||
(fn, s) => {
|
||||
fn.relation(s.comments).manyToOne(s.posts);
|
||||
fn.index(s.posts).on(["title"]);
|
||||
},
|
||||
);
|
||||
},
|
||||
onBoot: async () => {
|
||||
called.push("onBoot");
|
||||
},
|
||||
beforeBuild: async () => {
|
||||
called.push("beforeBuild");
|
||||
},
|
||||
onBuilt: async () => {
|
||||
called.push("onBuilt");
|
||||
},
|
||||
onServerInit: async (server) => {
|
||||
called.push("onServerInit");
|
||||
expect(server).toBeDefined();
|
||||
expect(server).toBeInstanceOf(Hono);
|
||||
},
|
||||
onFirstBoot: async () => {
|
||||
called.push("onFirstBoot");
|
||||
},
|
||||
};
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await app.module.auth.build();
|
||||
await app.module.data.build();
|
||||
console.log(app.em.entities.map((e) => e.name));
|
||||
console.log(await app.em.schema().getDiff());
|
||||
});*/
|
||||
|
||||
await app.build();
|
||||
|
||||
expect(app.em.entities.map((e) => e.name)).toEqual(["users", "posts", "comments"]);
|
||||
expect(app.em.indices.map((i) => i.name)).toEqual([
|
||||
"idx_unique_users_email",
|
||||
"idx_users_strategy",
|
||||
"idx_users_strategy_value",
|
||||
"idx_posts_title",
|
||||
]);
|
||||
expect(
|
||||
app.em.relations.all.map((r) => pick(r.toJSON(), ["type", "source", "target"])),
|
||||
).toEqual([
|
||||
{
|
||||
type: "n:1",
|
||||
source: "comments",
|
||||
target: "posts",
|
||||
},
|
||||
]);
|
||||
expect(called).toEqual([
|
||||
"onBoot",
|
||||
"onServerInit",
|
||||
"beforeBuild",
|
||||
"onServerInit",
|
||||
"schema",
|
||||
"onFirstBoot",
|
||||
"onBuilt",
|
||||
]);
|
||||
expect(app.plugins.size).toBe(1);
|
||||
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
|
||||
});
|
||||
|
||||
test.only("drivers", async () => {
|
||||
const called: string[] = [];
|
||||
const app = new App(dummyConnection, undefined, {
|
||||
drivers: {
|
||||
email: {
|
||||
send: async (to, subject, body) => {
|
||||
called.push("email.send");
|
||||
return {
|
||||
id: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
get: async (key) => {
|
||||
called.push("cache.get");
|
||||
return "";
|
||||
},
|
||||
set: async (key, value, ttl) => {
|
||||
called.push("cache.set");
|
||||
},
|
||||
del: async (key) => {
|
||||
called.push("cache.del");
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.build();
|
||||
|
||||
expect(app.drivers.cache).toBeDefined();
|
||||
expect(app.drivers.email).toBeDefined();
|
||||
await app.drivers.email.send("", "", "");
|
||||
await app.drivers.cache.get("");
|
||||
await app.drivers.cache.set("", "", 0);
|
||||
await app.drivers.cache.del("");
|
||||
|
||||
expect(called).toEqual(["email.send", "cache.get", "cache.set", "cache.del"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
tmp/*
|
||||
tmp/*
|
||||
!tmp/.gitkeep
|
||||
@@ -3,21 +3,36 @@ import * as adapter from "adapter";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { omitKeys } from "core/utils";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("adapter", () => {
|
||||
it("makes config", () => {
|
||||
expect(adapter.makeConfig({})).toEqual({});
|
||||
expect(adapter.makeConfig({}, { env: { TEST: "test" } })).toEqual({});
|
||||
expect(omitKeys(adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||
expect(omitKeys(adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"])).toEqual(
|
||||
{},
|
||||
);
|
||||
|
||||
// merges everything returned from `app` with the config
|
||||
expect(adapter.makeConfig({ app: (a) => a as any }, { env: { TEST: "test" } })).toEqual({
|
||||
env: { TEST: "test" },
|
||||
} as any);
|
||||
expect(
|
||||
omitKeys(
|
||||
adapter.makeConfig(
|
||||
{ app: (a) => ({ initialConfig: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||
{ env: { TEST: "test" } },
|
||||
),
|
||||
["connection"],
|
||||
),
|
||||
).toEqual({
|
||||
initialConfig: { server: { cors: { origin: "test" } } },
|
||||
});
|
||||
});
|
||||
|
||||
/* it.only("...", async () => {
|
||||
const app = await adapter.createAdapterApp();
|
||||
}); */
|
||||
|
||||
it("reuses apps correctly", async () => {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { Guard } from "../../src/auth";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import { Guard } from "../../src/auth/authorize/Guard";
|
||||
import { DataApi } from "../../src/data/api/DataApi";
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
import { schemaToEm } from "../helper";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||
import { parse } from "core/utils/schema";
|
||||
|
||||
beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);
|
||||
@@ -153,7 +153,7 @@ describe("DataApi", () => {
|
||||
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
||||
const oneByRes = await oneBy;
|
||||
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
||||
expect(oneByRes.body.meta.count).toEqual(1);
|
||||
expect(oneByRes.body.meta.items).toEqual(1);
|
||||
});
|
||||
|
||||
it("exists/count", async () => {
|
||||
@@ -202,7 +202,7 @@ describe("DataApi", () => {
|
||||
{
|
||||
// create many
|
||||
const res = await api.createMany("posts", payload);
|
||||
expect(res.data.length).toEqual(4);
|
||||
expect(res.data?.length).toEqual(4);
|
||||
expect(res.ok).toBeTrue();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import type { ModuleBuildContext } from "../../src";
|
||||
import { App, createApp } from "../../src/App";
|
||||
import { App, createApp } from "core/test/utils";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
|
||||
describe("App", () => {
|
||||
@@ -20,6 +20,7 @@ describe("App", () => {
|
||||
"guard",
|
||||
"flags",
|
||||
"logger",
|
||||
"helper",
|
||||
]);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { AppServer, serverConfigSchema } from "modules/server/AppServer";
|
||||
import { describe, test, expect } from "bun:test";
|
||||
|
||||
describe("AppServer", () => {
|
||||
test("config", () => {
|
||||
{
|
||||
const server = new AppServer();
|
||||
expect(server).toBeDefined();
|
||||
expect(server.config).toEqual({
|
||||
cors: {
|
||||
allow_credentials: true,
|
||||
origin: "*",
|
||||
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const server = new AppServer({
|
||||
cors: {
|
||||
origin: "https",
|
||||
allow_methods: ["GET", "POST"],
|
||||
},
|
||||
});
|
||||
expect(server).toBeDefined();
|
||||
expect(server.config).toEqual({
|
||||
cors: {
|
||||
allow_credentials: true,
|
||||
origin: "https",
|
||||
allow_methods: ["GET", "POST"],
|
||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
import { registries } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import * as proto from "../../src/data/prototype";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
|
||||
|
||||
@@ -1,46 +1,3 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Authenticator, type User, type UserPool } from "../../src/auth";
|
||||
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
|
||||
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
|
||||
import * as hash from "../../src/auth/utils/hash";
|
||||
import { Default, parse } from "../../src/core/utils";
|
||||
|
||||
/*class MemoryUserPool implements UserPool {
|
||||
constructor(private users: User[] = []) {}
|
||||
|
||||
async findBy(prop: "id" | "email" | "username", value: string | number) {
|
||||
return this.users.find((user) => user[prop] === value);
|
||||
}
|
||||
|
||||
async create(user: Pick<User, "email" | "password">) {
|
||||
const id = this.users.length + 1;
|
||||
const newUser = { ...user, id, username: user.email };
|
||||
this.users.push(newUser);
|
||||
return newUser;
|
||||
}
|
||||
}*/
|
||||
|
||||
describe("Authenticator", async () => {
|
||||
test("cookie options", async () => {
|
||||
console.log("parsed", parse(cookieConfig, undefined));
|
||||
console.log(Default(cookieConfig, {}));
|
||||
});
|
||||
/*const userpool = new MemoryUserPool([
|
||||
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
||||
]);
|
||||
|
||||
test("sha256 login", async () => {
|
||||
const auth = new Authenticator(userpool, {
|
||||
password: new PasswordStrategy({
|
||||
hashing: "sha256",
|
||||
}),
|
||||
});
|
||||
|
||||
const { token } = await auth.login("password", { email: "d", password: "test" });
|
||||
expect(token).toBeDefined();
|
||||
|
||||
const { iat, ...decoded } = decodeJwt<any>(token);
|
||||
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
|
||||
expect(await auth.verify(token)).toBe(true);
|
||||
});*/
|
||||
});
|
||||
describe("Authenticator", async () => {});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Guard } from "../../../src/auth";
|
||||
import { Guard } from "../../../src/auth/authorize/Guard";
|
||||
|
||||
describe("authorize", () => {
|
||||
test("basic", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type TObject, type TString, Type } from "@sinclair/typebox";
|
||||
import { Registry } from "core";
|
||||
import { Registry } from "core/registry/Registry";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
@@ -11,7 +11,7 @@ class What {
|
||||
return null;
|
||||
}
|
||||
getType() {
|
||||
return Type.Object({ type: Type.String() });
|
||||
return s.object({ type: s.string() });
|
||||
}
|
||||
}
|
||||
class What2 extends What {}
|
||||
@@ -19,7 +19,7 @@ class NotAllowed {}
|
||||
|
||||
type Test1 = {
|
||||
cls: new (...args: any[]) => What;
|
||||
schema: TObject<{ type: TString }>;
|
||||
schema: s.ObjectSchema<{ type: s.StringSchema }>;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("Registry", () => {
|
||||
const registry = new Registry<Test1>().set({
|
||||
first: {
|
||||
cls: What,
|
||||
schema: Type.Object({ type: Type.String(), what: Type.String() }),
|
||||
schema: s.object({ type: s.string(), what: s.string() }),
|
||||
enabled: true,
|
||||
},
|
||||
} satisfies Record<string, Test1>);
|
||||
@@ -37,7 +37,7 @@ describe("Registry", () => {
|
||||
expect(item).toBeDefined();
|
||||
expect(item?.cls).toBe(What);
|
||||
|
||||
const second = Type.Object({ type: Type.String(), what: Type.String() });
|
||||
const second = s.object({ type: s.string(), what: s.string() });
|
||||
registry.add("second", {
|
||||
cls: What2,
|
||||
schema: second,
|
||||
@@ -46,7 +46,7 @@ describe("Registry", () => {
|
||||
// @ts-ignore
|
||||
expect(registry.get("second").schema).toEqual(second);
|
||||
|
||||
const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() });
|
||||
const third = s.object({ type: s.string({ default: "1" }), what22: s.string() });
|
||||
registry.add("third", {
|
||||
// @ts-expect-error
|
||||
cls: NotAllowed,
|
||||
@@ -56,7 +56,7 @@ describe("Registry", () => {
|
||||
// @ts-ignore
|
||||
expect(registry.get("third").schema).toEqual(third);
|
||||
|
||||
const fourth = Type.Object({ type: Type.Number(), what22: Type.String() });
|
||||
const fourth = s.object({ type: s.number(), what22: s.string() });
|
||||
registry.add("fourth", {
|
||||
cls: What,
|
||||
// @ts-expect-error
|
||||
@@ -81,6 +81,8 @@ describe("Registry", () => {
|
||||
registry.register("what2", What2);
|
||||
expect(registry.get("what2")).toBeDefined();
|
||||
expect(registry.get("what2").cls).toBe(What2);
|
||||
expect(registry.get("what2").schema).toEqual(What2.prototype.getType());
|
||||
expect(JSON.stringify(registry.get("what2").schema)).toEqual(
|
||||
JSON.stringify(What2.prototype.getType()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { SchemaObject } from "../../../src/core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { s } from "core/utils/schema";
|
||||
import { SchemaObject } from "core/object/SchemaObject";
|
||||
|
||||
describe("SchemaObject", async () => {
|
||||
test("basic", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({ a: Type.String({ default: "b" }) }),
|
||||
s.strictObject({ a: s.string({ default: "b" }) }),
|
||||
{ a: "test" },
|
||||
{
|
||||
forceParse: true,
|
||||
@@ -23,19 +23,19 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("patch", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.strictObject({
|
||||
s: s.strictObject(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.strictObject(
|
||||
{
|
||||
c: Type.String({ default: "d" }),
|
||||
e: Type.String({ default: "f" }),
|
||||
c: s.string({ default: "d" }),
|
||||
e: s.string({ default: "f" }),
|
||||
},
|
||||
{ default: {} },
|
||||
),
|
||||
},
|
||||
{ default: {}, additionalProperties: false },
|
||||
{ default: {} },
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -44,7 +44,7 @@ describe("SchemaObject", async () => {
|
||||
await m.patch("s.a", "c");
|
||||
|
||||
// non-existing path on no additional properties
|
||||
expect(() => m.patch("s.s.s", "c")).toThrow();
|
||||
expect(m.patch("s.s.s", "c")).rejects.toThrow();
|
||||
// wrong type
|
||||
expect(() => m.patch("s.a", 1)).toThrow();
|
||||
|
||||
@@ -58,8 +58,8 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("patch array", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||
@@ -75,13 +75,13 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("remove", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.object({
|
||||
s: s.object(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.object(
|
||||
{
|
||||
c: Type.String({ default: "d" }),
|
||||
c: s.string({ default: "d" }),
|
||||
},
|
||||
{ default: {} },
|
||||
),
|
||||
@@ -107,8 +107,8 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("set", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
);
|
||||
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
|
||||
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
|
||||
let called = false;
|
||||
let result: any;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
|
||||
test("listener: onBeforeUpdate", async () => {
|
||||
let called = false;
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
|
||||
s.strictObject({
|
||||
methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
@@ -167,7 +167,7 @@ describe("SchemaObject", async () => {
|
||||
});
|
||||
|
||||
test("throwIfRestricted", async () => {
|
||||
const m = new SchemaObject(Type.Object({}), undefined, {
|
||||
const m = new SchemaObject(s.strictObject({}), undefined, {
|
||||
restrictPaths: ["a.b"],
|
||||
});
|
||||
|
||||
@@ -179,13 +179,13 @@ describe("SchemaObject", async () => {
|
||||
|
||||
test("restriction bypass", async () => {
|
||||
const m = new SchemaObject(
|
||||
Type.Object({
|
||||
s: Type.Object(
|
||||
s.strictObject({
|
||||
s: s.strictObject(
|
||||
{
|
||||
a: Type.String({ default: "b" }),
|
||||
b: Type.Object(
|
||||
a: s.string({ default: "b" }),
|
||||
b: s.strictObject(
|
||||
{
|
||||
c: Type.String({ default: "d" }),
|
||||
c: s.string({ default: "d" }),
|
||||
},
|
||||
{ default: {} },
|
||||
),
|
||||
@@ -205,7 +205,21 @@ describe("SchemaObject", async () => {
|
||||
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
|
||||
});
|
||||
|
||||
const dataEntitiesSchema = Type.Object(
|
||||
const dataEntitiesSchema = s.strictObject({
|
||||
entities: s.record(
|
||||
s.object({
|
||||
fields: s.record(
|
||||
s.object({
|
||||
type: s.string(),
|
||||
config: s.object({}).optional(),
|
||||
}),
|
||||
),
|
||||
config: s.record(s.string()).optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
/* const dataEntitiesSchema = Type.Object(
|
||||
{
|
||||
entities: Type.Object(
|
||||
{},
|
||||
@@ -230,7 +244,7 @@ describe("SchemaObject", async () => {
|
||||
{
|
||||
additionalProperties: false,
|
||||
},
|
||||
);
|
||||
); */
|
||||
test("patch safe object, overwrite", async () => {
|
||||
const data = {
|
||||
entities: {
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
|
||||
import { Guard } from "../../src/auth";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import {
|
||||
Entity,
|
||||
type EntityData,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
type MutatorResponse,
|
||||
type RepositoryResponse,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { Guard } from "../../src/auth/authorize/Guard";
|
||||
import { parse } from "core/utils/schema";
|
||||
|
||||
import { DataController } from "../../src/data/api/DataController";
|
||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
||||
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
||||
import { Entity, EntityManager, type EntityData } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||
@@ -21,52 +18,6 @@ afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
const dataConfig = parse(dataConfigSchema, {});
|
||||
describe("[data] DataController", async () => {
|
||||
test("repoResult", async () => {
|
||||
const em = new EntityManager<any>([], dummyConnection);
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
|
||||
const res = controller.repoResult({
|
||||
entity: null as any,
|
||||
data: [] as any,
|
||||
sql: "",
|
||||
parameters: [] as any,
|
||||
result: [] as any,
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
meta: {
|
||||
total: 0,
|
||||
count: 0,
|
||||
items: 0,
|
||||
},
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("mutatorResult", async () => {
|
||||
const em = new EntityManager([], dummyConnection);
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
|
||||
const res = controller.mutatorResult({
|
||||
entity: null as any,
|
||||
data: [] as any,
|
||||
sql: "",
|
||||
parameters: [] as any,
|
||||
result: [] as any,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe("getController", async () => {
|
||||
const users = new Entity("users", [
|
||||
new TextField("name", { required: true }),
|
||||
@@ -120,8 +71,7 @@ describe("[data] DataController", async () => {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_user),
|
||||
});
|
||||
//console.log("res", { _user }, res);
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const result = (await res.json()) as MutatorResultJSON;
|
||||
const { id, ...data } = result.data as any;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
@@ -135,7 +85,7 @@ describe("[data] DataController", async () => {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_post),
|
||||
});
|
||||
const result = (await res.json()) as MutatorResponse;
|
||||
const result = (await res.json()) as MutatorResultJSON;
|
||||
const { id, ...data } = result.data as any;
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
@@ -146,13 +96,13 @@ describe("[data] DataController", async () => {
|
||||
|
||||
test("/:entity (read many)", async () => {
|
||||
const res = await app.request("/entity/users");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(3);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).toBe(3);
|
||||
expect(data.data.length).toBe(3);
|
||||
expect(data.data[0].name).toBe("foo");
|
||||
expect(data.data[0]?.name).toBe("foo");
|
||||
});
|
||||
|
||||
test("/:entity/query (func query)", async () => {
|
||||
@@ -165,33 +115,32 @@ describe("[data] DataController", async () => {
|
||||
where: { bio: { $isnull: 1 } },
|
||||
}),
|
||||
});
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].name).toBe("bar");
|
||||
expect(data.data[0]?.name).toBe("bar");
|
||||
});
|
||||
|
||||
test("/:entity (read many, paginated)", async () => {
|
||||
const res = await app.request("/entity/users?limit=1&offset=2");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(3);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(3);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].name).toBe("baz");
|
||||
expect(data.data[0]?.name).toBe("baz");
|
||||
});
|
||||
|
||||
test("/:entity/:id (read one)", async () => {
|
||||
const res = await app.request("/entity/users/3");
|
||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
console.log("data", data);
|
||||
const data = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(3);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
|
||||
});
|
||||
@@ -201,7 +150,7 @@ describe("[data] DataController", async () => {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ name: "new name" }),
|
||||
});
|
||||
const { data } = (await res.json()) as MutatorResponse;
|
||||
const { data } = (await res.json()) as MutatorResultJSON;
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
|
||||
@@ -209,27 +158,26 @@ describe("[data] DataController", async () => {
|
||||
|
||||
test("/:entity/:id/:reference (read references)", async () => {
|
||||
const res = await app.request("/entity/users/1/posts");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
console.log("data", data);
|
||||
const data = (await res.json()) as RepositoryResultJSON;
|
||||
|
||||
expect(data.meta.total).toBe(2);
|
||||
expect(data.meta.count).toBe(1);
|
||||
//expect(data.meta.total).toBe(2);
|
||||
//expect(data.meta.count).toBe(1);
|
||||
expect(data.meta.items).toBe(1);
|
||||
expect(data.data.length).toBe(1);
|
||||
expect(data.data[0].content).toBe("post 1");
|
||||
expect(data.data[0]?.content).toBe("post 1");
|
||||
});
|
||||
|
||||
test("/:entity/:id (delete one)", async () => {
|
||||
const res = await app.request("/entity/posts/2", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
const { data } = (await res.json()) as RepositoryResultJSON<EntityData>;
|
||||
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||
|
||||
// verify
|
||||
const res2 = await app.request("/entity/posts");
|
||||
const data2 = (await res2.json()) as RepositoryResponse;
|
||||
expect(data2.meta.total).toBe(1);
|
||||
const data2 = (await res2.json()) as RepositoryResultJSON;
|
||||
//expect(data2.meta.total).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
NumberField,
|
||||
PrimaryField,
|
||||
Repository,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, PrimaryField, NumberField } from "data/fields";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -34,19 +28,12 @@ describe("some tests", async () => {
|
||||
|
||||
test("findId", async () => {
|
||||
const query = await em.repository(users).findId(1);
|
||||
/*const { result, total, count, time } = query;
|
||||
console.log("query", query.result, {
|
||||
result,
|
||||
total,
|
||||
count,
|
||||
time,
|
||||
});*/
|
||||
|
||||
expect(query.sql).toBe(
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([1, 1]);
|
||||
expect(query.result).toEqual([]);
|
||||
expect(query.data).toBeUndefined();
|
||||
});
|
||||
|
||||
test("findMany", async () => {
|
||||
@@ -56,7 +43,7 @@ describe("some tests", async () => {
|
||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
expect(query.data).toEqual([]);
|
||||
});
|
||||
|
||||
test("findMany with number", async () => {
|
||||
@@ -66,7 +53,7 @@ describe("some tests", async () => {
|
||||
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
|
||||
);
|
||||
expect(query.parameters).toEqual([10, 0]);
|
||||
expect(query.result).toEqual([]);
|
||||
expect(query.data).toEqual([]);
|
||||
});
|
||||
|
||||
test("try adding an existing field name", async () => {
|
||||
@@ -110,4 +97,18 @@ describe("some tests", async () => {
|
||||
new EntityManager([entity, entity2], connection);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("primary uuid", async () => {
|
||||
const entity = new Entity("users", [
|
||||
new PrimaryField("id", { format: "uuid" }),
|
||||
new TextField("username"),
|
||||
]);
|
||||
const em = new EntityManager([entity], getDummyConnection().dummyConnection);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const mutator = em.mutator(entity);
|
||||
const data = await mutator.insertOne({ username: "test" });
|
||||
expect(data.data.id).toBeDefined();
|
||||
expect(data.data.id).toBeString();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
|
||||
import type { SqliteDatabase } from "kysely";
|
||||
// @ts-ignore
|
||||
import Database from "libsql";
|
||||
import { SqliteLocalConnection } from "../../src/data";
|
||||
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
SchemaManager,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { Entity } from "data/entities";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
||||
import { TransformPersistFailedException } from "../../src/data/errors";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import { TransformPersistFailedException } from "data/errors";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -45,7 +46,7 @@ describe("Mutator simple", async () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||
expect(query.data).toEqual([{ id: 1, label: "test", count: 1 }]);
|
||||
});
|
||||
|
||||
test("update inserted row", async () => {
|
||||
@@ -87,7 +88,7 @@ describe("Mutator simple", async () => {
|
||||
expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
|
||||
|
||||
const query2 = await em.repository(items).findId(id);
|
||||
expect(query2.result.length).toBe(0);
|
||||
expect(query2.data).toBeUndefined();
|
||||
});
|
||||
|
||||
test("validation: insert incomplete row", async () => {
|
||||
@@ -177,13 +178,13 @@ describe("Mutator simple", async () => {
|
||||
});
|
||||
|
||||
test("insertMany", async () => {
|
||||
const oldCount = (await em.repo(items).count()).count;
|
||||
const oldCount = (await em.repo(items).count()).data.count;
|
||||
const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
|
||||
const { data } = await em.mutator(items).insertMany(inserts);
|
||||
|
||||
expect(data.length).toBe(2);
|
||||
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
|
||||
const newCount = (await em.repo(items).count()).count;
|
||||
const newCount = (await em.repo(items).count()).data.count;
|
||||
expect(newCount).toBe(oldCount + inserts.length);
|
||||
|
||||
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||
import { stripMark } from "../../src/core/utils";
|
||||
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
||||
import { stripMark } from "core/utils/schema";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import { PolymorphicRelation } from "data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -2,19 +2,20 @@ import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
BooleanField,
|
||||
DateField,
|
||||
Entity,
|
||||
EntityIndex,
|
||||
EntityManager,
|
||||
EnumField,
|
||||
JsonField,
|
||||
NumberField,
|
||||
TextField,
|
||||
EntityIndex,
|
||||
} from "data/fields";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField,
|
||||
} from "../../src/data";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
} from "data/relations";
|
||||
import { DummyConnection } from "data/connection/DummyConnection";
|
||||
import {
|
||||
FieldPrototype,
|
||||
type FieldSchema,
|
||||
@@ -32,8 +33,8 @@ import {
|
||||
number,
|
||||
relation,
|
||||
text,
|
||||
} from "../../src/data/prototype";
|
||||
import { MediaField } from "../../src/media/MediaField";
|
||||
} from "data/prototype";
|
||||
import { MediaField } from "media/MediaField";
|
||||
|
||||
describe("prototype", () => {
|
||||
test("...", () => {
|
||||
@@ -101,7 +102,8 @@ describe("prototype", () => {
|
||||
|
||||
type Posts = Schema<typeof posts2>;
|
||||
|
||||
expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||
// @todo: check
|
||||
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
|
||||
});
|
||||
|
||||
test("test example", async () => {
|
||||
@@ -295,9 +297,9 @@ describe("prototype", () => {
|
||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||
new Entity("comments", [new TextField("some")]),
|
||||
new Entity("users", [new TextField("email")]),
|
||||
];
|
||||
] as const;
|
||||
const _em2 = new EntityManager(
|
||||
es,
|
||||
[...es],
|
||||
new DummyConnection(),
|
||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
||||
[
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, TextField } from "../../src/data";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField } from "data/fields";
|
||||
import {
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
OneToOneRelation,
|
||||
PolymorphicRelation,
|
||||
RelationField,
|
||||
} from "../../src/data/relations";
|
||||
} from "data/relations";
|
||||
import { getDummyConnection } from "./helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
@@ -77,7 +78,7 @@ describe("Relations", async () => {
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// verify naming
|
||||
const rel = em.relations.all[0];
|
||||
const rel = em.relations.all[0]!;
|
||||
expect(rel.source.entity.name).toBe(posts.name);
|
||||
expect(rel.source.reference).toBe(posts.name);
|
||||
expect(rel.target.entity.name).toBe(users.name);
|
||||
@@ -89,11 +90,11 @@ describe("Relations", async () => {
|
||||
// verify low level relation
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
||||
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userPostsRel = em.relationOf(users.name, "posts");
|
||||
@@ -191,7 +192,7 @@ describe("Relations", async () => {
|
||||
const em = new EntityManager(entities, dummyConnection, relations);
|
||||
|
||||
// verify naming
|
||||
const rel = em.relations.all[0];
|
||||
const rel = em.relations.all[0]!;
|
||||
expect(rel.source.entity.name).toBe(users.name);
|
||||
expect(rel.source.reference).toBe(users.name);
|
||||
expect(rel.target.entity.name).toBe(settings.name);
|
||||
@@ -202,8 +203,8 @@ describe("Relations", async () => {
|
||||
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name).length).toBe(1);
|
||||
expect(em.relationsOf(users.name)[0].source.entity).toBe(users);
|
||||
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
|
||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(users);
|
||||
expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings);
|
||||
|
||||
// verify high level relation (from users)
|
||||
const userSettingRel = em.relationOf(users.name, settings.name);
|
||||
@@ -323,7 +324,7 @@ describe("Relations", async () => {
|
||||
);
|
||||
|
||||
// mutation info
|
||||
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({
|
||||
reference: "categories",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
@@ -334,7 +335,7 @@ describe("Relations", async () => {
|
||||
cardinality: undefined,
|
||||
relation_type: "m:n",
|
||||
});
|
||||
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
expect(relations[0]!.helper(categories.name)!.getMutationInfo()).toEqual({
|
||||
reference: "posts",
|
||||
local_field: undefined,
|
||||
$set: false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Entity, NumberField, TextField } from "data";
|
||||
import * as p from "data/prototype";
|
||||
import { Entity } from "data/entities";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
|
||||
describe("[data] Entity", async () => {
|
||||
const entity = new Entity("test", [
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
SchemaManager,
|
||||
} from "../../../src/data";
|
||||
import { UnableToConnectException } from "../../../src/data/errors";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { UnableToConnectException } from "data/errors";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
|
||||
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { TextField } from "data/fields";
|
||||
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import type { EventManager } from "../../../src/core/events";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToOneRelation,
|
||||
MutatorEvents,
|
||||
NumberField,
|
||||
OneToOneRelation,
|
||||
type RelationField,
|
||||
RelationField,
|
||||
RelationMutator,
|
||||
TextField,
|
||||
} from "../../../src/data";
|
||||
import * as proto from "../../../src/data/prototype";
|
||||
import { getDummyConnection } from "../helper";
|
||||
} from "data/relations";
|
||||
import { NumberField, TextField } from "data/fields";
|
||||
import * as proto from "data/prototype";
|
||||
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
|
||||
import { MutatorEvents } from "data/events";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
afterAll(afterAllCleanup);
|
||||
|
||||
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||
|
||||
describe("[data] Mutator (base)", async () => {
|
||||
const entity = new Entity("items", [
|
||||
new TextField("label", { required: true }),
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import type { Kysely, Transaction } from "kysely";
|
||||
import { Perf } from "core/utils";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
LibsqlConnection,
|
||||
ManyToOneRelation,
|
||||
RepositoryEvents,
|
||||
TextField,
|
||||
entity as $entity,
|
||||
text as $text,
|
||||
em as $em,
|
||||
} from "data";
|
||||
import { TextField } from "data/fields";
|
||||
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToOneRelation } from "data/relations";
|
||||
import { RepositoryEvents } from "data/events";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
type E = Kysely<any> | Transaction<any>;
|
||||
@@ -26,120 +19,6 @@ async function sleep(ms: number) {
|
||||
}
|
||||
|
||||
describe("[Repository]", async () => {
|
||||
test.skip("bulk", async () => {
|
||||
//const connection = dummyConnection;
|
||||
//const connection = getLocalLibsqlConnection();
|
||||
const credentials = null as any; // @todo: determine what to do here
|
||||
const connection = new LibsqlConnection(credentials);
|
||||
|
||||
const em = new EntityManager([], connection);
|
||||
/*const emLibsql = new EntityManager([], {
|
||||
url: connection.url.replace("https", "libsql"),
|
||||
authToken: connection.authToken,
|
||||
});*/
|
||||
const table = "posts";
|
||||
|
||||
const client = connection.getClient();
|
||||
if (!client) {
|
||||
console.log("Cannot perform test without libsql connection");
|
||||
return;
|
||||
}
|
||||
|
||||
const conn = em.connection.kysely;
|
||||
const selectQ = (e: E) => e.selectFrom(table).selectAll().limit(2);
|
||||
const countQ = (e: E) => e.selectFrom(table).select(e.fn.count("*").as("count"));
|
||||
|
||||
async function executeTransaction(em: EntityManager<any>) {
|
||||
return await em.connection.kysely.transaction().execute(async (e) => {
|
||||
const res = await selectQ(e).execute();
|
||||
const count = await countQ(e).execute();
|
||||
|
||||
return [res, count];
|
||||
});
|
||||
}
|
||||
|
||||
async function executeBatch(em: EntityManager<any>) {
|
||||
const queries = [selectQ(conn), countQ(conn)];
|
||||
return await em.connection.batchQuery(queries);
|
||||
}
|
||||
|
||||
async function executeSingleKysely(em: EntityManager<any>) {
|
||||
const res = await selectQ(conn).execute();
|
||||
const count = await countQ(conn).execute();
|
||||
return [res, count];
|
||||
}
|
||||
|
||||
async function executeSingleClient(em: EntityManager<any>) {
|
||||
const q1 = selectQ(conn).compile();
|
||||
const res = await client.execute({
|
||||
sql: q1.sql,
|
||||
args: q1.parameters as any,
|
||||
});
|
||||
|
||||
const q2 = countQ(conn).compile();
|
||||
const count = await client.execute({
|
||||
sql: q2.sql,
|
||||
args: q2.parameters as any,
|
||||
});
|
||||
return [res, count];
|
||||
}
|
||||
|
||||
const transaction = await executeTransaction(em);
|
||||
const batch = await executeBatch(em);
|
||||
|
||||
expect(batch).toEqual(transaction as any);
|
||||
|
||||
const testperf = false;
|
||||
if (testperf) {
|
||||
const times = 5;
|
||||
|
||||
const exec = async (
|
||||
name: string,
|
||||
fn: (em: EntityManager<any>) => Promise<any>,
|
||||
em: EntityManager<any>,
|
||||
) => {
|
||||
const res = await Perf.execute(() => fn(em), times);
|
||||
await sleep(1000);
|
||||
const info = {
|
||||
name,
|
||||
total: res.total.toFixed(2),
|
||||
avg: (res.total / times).toFixed(2),
|
||||
first: res.marks[0].time.toFixed(2),
|
||||
last: res.marks[res.marks.length - 1].time.toFixed(2),
|
||||
};
|
||||
console.log(info.name, info, res.marks);
|
||||
return info;
|
||||
};
|
||||
|
||||
const data: any[] = [];
|
||||
data.push(await exec("transaction.http", executeTransaction, em));
|
||||
data.push(await exec("bulk.http", executeBatch, em));
|
||||
data.push(await exec("singleKy.http", executeSingleKysely, em));
|
||||
data.push(await exec("singleCl.http", executeSingleClient, em));
|
||||
|
||||
/*data.push(await exec("transaction.libsql", executeTransaction, emLibsql));
|
||||
data.push(await exec("bulk.libsql", executeBatch, emLibsql));
|
||||
data.push(await exec("singleKy.libsql", executeSingleKysely, emLibsql));
|
||||
data.push(await exec("singleCl.libsql", executeSingleClient, emLibsql));*/
|
||||
|
||||
console.table(data);
|
||||
/**
|
||||
* ┌───┬────────────────────┬────────┬────────┬────────┬────────┐
|
||||
* │ │ name │ total │ avg │ first │ last │
|
||||
* ├───┼────────────────────┼────────┼────────┼────────┼────────┤
|
||||
* │ 0 │ transaction.http │ 681.29 │ 136.26 │ 136.46 │ 396.09 │
|
||||
* │ 1 │ bulk.http │ 164.82 │ 32.96 │ 32.95 │ 99.91 │
|
||||
* │ 2 │ singleKy.http │ 330.01 │ 66.00 │ 65.86 │ 195.41 │
|
||||
* │ 3 │ singleCl.http │ 326.17 │ 65.23 │ 61.32 │ 198.08 │
|
||||
* │ 4 │ transaction.libsql │ 856.79 │ 171.36 │ 132.31 │ 595.24 │
|
||||
* │ 5 │ bulk.libsql │ 180.63 │ 36.13 │ 35.39 │ 107.71 │
|
||||
* │ 6 │ singleKy.libsql │ 347.11 │ 69.42 │ 65.00 │ 207.14 │
|
||||
* │ 7 │ singleCl.libsql │ 328.60 │ 65.72 │ 62.19 │ 195.04 │
|
||||
* └───┴────────────────────┴────────┴────────┴────────┴────────┘
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
test("count & exists", async () => {
|
||||
const items = new Entity("items", [new TextField("label")]);
|
||||
const em = new EntityManager([items], dummyConnection);
|
||||
@@ -160,25 +39,44 @@ describe("[Repository]", async () => {
|
||||
// count all
|
||||
const res = await em.repository(items).count();
|
||||
expect(res.sql).toBe('select count(*) as "count" from "items"');
|
||||
expect(res.count).toBe(3);
|
||||
expect(res.data.count).toBe(3);
|
||||
|
||||
//
|
||||
{
|
||||
const res = await em.repository(items).findMany();
|
||||
expect(res.count).toBe(3);
|
||||
}
|
||||
|
||||
{
|
||||
const res = await em
|
||||
.repository(items, {
|
||||
includeCounts: true,
|
||||
})
|
||||
.findMany();
|
||||
expect(res.count).toBe(3);
|
||||
}
|
||||
|
||||
// count filtered
|
||||
const res2 = await em.repository(items).count({ label: { $in: ["a", "b"] } });
|
||||
const res2 = await em
|
||||
.repository(items, {
|
||||
includeCounts: true,
|
||||
})
|
||||
.count({ label: { $in: ["a", "b"] } });
|
||||
|
||||
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
|
||||
expect(res2.parameters).toEqual(["a", "b"]);
|
||||
expect(res2.count).toBe(2);
|
||||
expect(res2.data.count).toBe(2);
|
||||
|
||||
// check exists
|
||||
const res3 = await em.repository(items).exists({ label: "a" });
|
||||
expect(res3.exists).toBe(true);
|
||||
expect(res3.data.exists).toBe(true);
|
||||
|
||||
const res4 = await em.repository(items).exists({ label: "d" });
|
||||
expect(res4.exists).toBe(false);
|
||||
expect(res4.data.exists).toBe(false);
|
||||
|
||||
// for now, allow empty filter
|
||||
const res5 = await em.repository(items).exists({});
|
||||
expect(res5.exists).toBe(true);
|
||||
expect(res5.data.exists).toBe(true);
|
||||
});
|
||||
|
||||
test("option: silent", async () => {
|
||||
@@ -191,6 +89,9 @@ describe("[Repository]", async () => {
|
||||
// should throw because table doesn't exist
|
||||
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
|
||||
// should silently return empty result
|
||||
em.repo("items", { silent: true })
|
||||
.findMany({})
|
||||
.then((r) => r.data);
|
||||
expect(
|
||||
em
|
||||
.repo("items", { silent: true })
|
||||
@@ -209,16 +110,16 @@ describe("[Repository]", async () => {
|
||||
|
||||
expect(
|
||||
em
|
||||
.repo("items")
|
||||
.repo("items", { includeCounts: true })
|
||||
.findMany({})
|
||||
.then((r) => [r.meta.count, r.meta.total]),
|
||||
.then((r) => [r.count, r.total]),
|
||||
).resolves.toEqual([0, 0]);
|
||||
|
||||
expect(
|
||||
em
|
||||
.repo("items", { includeCounts: false })
|
||||
.findMany({})
|
||||
.then((r) => [r.meta.count, r.meta.total]),
|
||||
.then((r) => [r.count, r.total]),
|
||||
).resolves.toEqual([undefined, undefined]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { randomString } from "../../../src/core/utils";
|
||||
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
||||
import { randomString } from "core/utils";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { TextField, EntityIndex } from "data/fields";
|
||||
import { SchemaManager } from "data/schema/SchemaManager";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { type WhereQuery, WhereBuilder } from "data";
|
||||
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
||||
|
||||
function qb() {
|
||||
const c = getDummyConnection();
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToManyRelation,
|
||||
ManyToOneRelation,
|
||||
PolymorphicRelation,
|
||||
TextField,
|
||||
WithBuilder,
|
||||
} from "../../../src/data";
|
||||
import * as proto from "../../../src/data/prototype";
|
||||
import { Entity, EntityManager } from "data/entities";
|
||||
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
||||
import { TextField } from "data/fields";
|
||||
import * as proto from "data/prototype";
|
||||
import { WithBuilder } from "data/entities/query/WithBuilder";
|
||||
import { schemaToEm } from "../../helper";
|
||||
import { getDummyConnection } from "../helper";
|
||||
|
||||
@@ -89,9 +84,9 @@ describe("[data] WithBuilder", async () => {
|
||||
const res2 = qb2.compile();
|
||||
|
||||
expect(res2.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"',
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ?) as obj) as "author" from "posts"',
|
||||
);
|
||||
expect(res2.parameters).toEqual([1, 0]);
|
||||
expect(res2.parameters).toEqual([1]);
|
||||
});
|
||||
|
||||
test("test with empty join", async () => {
|
||||
@@ -194,9 +189,9 @@ describe("[data] WithBuilder", async () => {
|
||||
);
|
||||
const res = qb.compile();
|
||||
expect(res.sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"',
|
||||
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "single" from "categories"',
|
||||
);
|
||||
expect(res.parameters).toEqual(["categories.single", 1, 0]);
|
||||
expect(res.parameters).toEqual(["categories.single", 1]);
|
||||
|
||||
const qb2 = WithBuilder.addClause(
|
||||
em,
|
||||
@@ -273,9 +268,9 @@ describe("[data] WithBuilder", async () => {
|
||||
|
||||
//prettyPrintQb(qb);
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"',
|
||||
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts"',
|
||||
);
|
||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
|
||||
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]);
|
||||
});
|
||||
|
||||
test("compiles with many", async () => {
|
||||
@@ -315,9 +310,9 @@ describe("[data] WithBuilder", async () => {
|
||||
);
|
||||
|
||||
expect(qb.compile().sql).toBe(
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
||||
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
|
||||
);
|
||||
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
|
||||
expect(qb.compile().parameters).toEqual([1, 12, 0]);
|
||||
});
|
||||
|
||||
test("returns correct result", async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { EntityManager } from "../../../../src/data";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { getDummyConnection } from "../../helper";
|
||||
|
||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { BooleanField } from "../../../../src/data";
|
||||
import { BooleanField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] BooleanField", async () => {
|
||||
fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||
|
||||
test("transformRetrieve", async () => {
|
||||
const field = new BooleanField("test");
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { DateField } from "../../../../src/data";
|
||||
import { describe, test } from "bun:test";
|
||||
import { DateField } from "data/fields";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
describe("[data] DateField", async () => {
|
||||
fieldTestSuite({ expect, test }, DateField, { defaultValue: new Date(), schemaType: "date" });
|
||||
fieldTestSuite(
|
||||
bunTestRunner,
|
||||
DateField,
|
||||
{ defaultValue: new Date(), schemaType: "date" },
|
||||
{ type: "date" },
|
||||
);
|
||||
|
||||
// @todo: add datefield tests
|
||||
test("week", async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { EnumField } from "../../../../src/data";
|
||||
import { EnumField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
function options(strings: string[]) {
|
||||
@@ -8,7 +9,7 @@ function options(strings: string[]) {
|
||||
|
||||
describe("[data] EnumField", async () => {
|
||||
fieldTestSuite(
|
||||
{ expect, test },
|
||||
bunTestRunner,
|
||||
// @ts-ignore
|
||||
EnumField,
|
||||
{ defaultValue: "a", schemaType: "text" },
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Default, stripMark } from "../../../../src/core/utils";
|
||||
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { stripMark } from "core/utils/schema";
|
||||
|
||||
describe("[data] Field", async () => {
|
||||
class FieldSpec extends Field {
|
||||
@@ -19,10 +20,10 @@ describe("[data] Field", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
fieldTestSuite({ expect, test }, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||
fieldTestSuite(bunTestRunner, FieldSpec, { defaultValue: "test", schemaType: "text" });
|
||||
|
||||
test("default config", async () => {
|
||||
const config = Default(baseFieldConfigSchema, {});
|
||||
const config = baseFieldConfigSchema.template({});
|
||||
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { Entity, EntityIndex, Field } from "../../../../src/data";
|
||||
import { Entity } from "data/entities";
|
||||
import { Field, EntityIndex } from "data/fields";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
class TestField extends Field {
|
||||
protected getSchema(): any {
|
||||
return Type.Any();
|
||||
return s.any();
|
||||
}
|
||||
|
||||
override schema() {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { JsonField } from "../../../../src/data";
|
||||
import { JsonField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] JsonField", async () => {
|
||||
const field = new JsonField("test");
|
||||
fieldTestSuite({ expect, test }, JsonField, {
|
||||
fieldTestSuite(bunTestRunner, JsonField, {
|
||||
defaultValue: { a: 1 },
|
||||
sampleValues: ["string", { test: 1 }, 1],
|
||||
schemaType: "text",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { JsonSchemaField } from "../../../../src/data";
|
||||
import { JsonSchemaField } from "data/fields";
|
||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] JsonSchemaField", async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { NumberField } from "../../../../src/data";
|
||||
import { NumberField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
|
||||
describe("[data] NumberField", async () => {
|
||||
@@ -15,5 +16,5 @@ describe("[data] NumberField", async () => {
|
||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
||||
});
|
||||
|
||||
fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||
fieldTestSuite(bunTestRunner, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { PrimaryField } from "../../../../src/data";
|
||||
import { PrimaryField } from "data/fields";
|
||||
|
||||
describe("[data] PrimaryField", async () => {
|
||||
const field = new PrimaryField("primary");
|
||||
@@ -39,4 +39,28 @@ describe("[data] PrimaryField", async () => {
|
||||
expect(field.transformPersist(1)).rejects.toThrow();
|
||||
expect(field.transformRetrieve(1)).toBe(1);
|
||||
});
|
||||
|
||||
test("format", () => {
|
||||
const uuid = new PrimaryField("uuid", { format: "uuid" });
|
||||
expect(uuid.format).toBe("uuid");
|
||||
expect(uuid.fieldType).toBe("text");
|
||||
expect(uuid.getNewValue()).toBeString();
|
||||
expect(uuid.toType()).toEqual({
|
||||
required: true,
|
||||
comment: undefined,
|
||||
type: "Generated<string>",
|
||||
import: [{ package: "kysely", name: "Generated" }],
|
||||
});
|
||||
|
||||
const integer = new PrimaryField("integer", { format: "integer" });
|
||||
expect(integer.format).toBe("integer");
|
||||
expect(integer.fieldType).toBe("integer");
|
||||
expect(integer.getNewValue()).toBeUndefined();
|
||||
expect(integer.toType()).toEqual({
|
||||
required: true,
|
||||
comment: undefined,
|
||||
type: "Generated<number>",
|
||||
import: [{ package: "kysely", name: "Generated" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { TextField } from "../../../../src/data";
|
||||
import { TextField } from "data/fields";
|
||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
|
||||
describe("[data] TextField", async () => {
|
||||
test("transformPersist (config)", async () => {
|
||||
@@ -11,5 +12,5 @@ describe("[data] TextField", async () => {
|
||||
expect(transformPersist(field, "abc")).resolves.toBe("abc");
|
||||
});
|
||||
|
||||
fieldTestSuite({ expect, test }, TextField, { defaultValue: "abc", schemaType: "text" });
|
||||
fieldTestSuite(bunTestRunner, TextField, { defaultValue: "abc", schemaType: "text" });
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it, test } from "bun:test";
|
||||
import { Entity, type EntityManager } from "../../../../src/data";
|
||||
import { Entity, type EntityManager } from "data/entities";
|
||||
import {
|
||||
type BaseRelationConfig,
|
||||
EntityRelation,
|
||||
EntityRelationAnchor,
|
||||
RelationTypes,
|
||||
} from "../../../../src/data/relations";
|
||||
} from "data/relations";
|
||||
|
||||
class TestEntityRelation extends EntityRelation {
|
||||
constructor(config?: BaseRelationConfig) {
|
||||
@@ -24,11 +24,11 @@ class TestEntityRelation extends EntityRelation {
|
||||
return this;
|
||||
}
|
||||
|
||||
buildWith(a: any, b: any, c: any): any {
|
||||
buildWith(): any {
|
||||
return;
|
||||
}
|
||||
|
||||
buildJoin(a: any, b: any): any {
|
||||
buildJoin(): any {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ beforeAll(() =>
|
||||
);
|
||||
afterAll(unmockFetch);
|
||||
|
||||
describe("FetchTask", async () => {
|
||||
describe.skip("FetchTask", async () => {
|
||||
test("Simple fetch", async () => {
|
||||
const task = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
export class StringifyTask<Output extends string> extends Task<
|
||||
typeof StringifyTask.schema,
|
||||
@@ -8,18 +8,16 @@ export class StringifyTask<Output extends string> extends Task<
|
||||
> {
|
||||
type = "stringify";
|
||||
|
||||
static override schema = Type.Optional(
|
||||
Type.Object({
|
||||
input: Type.Optional(Type.String()),
|
||||
}),
|
||||
);
|
||||
static override schema = s.object({
|
||||
input: s.string().optional(),
|
||||
});
|
||||
|
||||
async execute() {
|
||||
return JSON.stringify(this.params.input) as Output;
|
||||
}
|
||||
}
|
||||
|
||||
describe("SubFlowTask", async () => {
|
||||
describe.skip("SubFlowTask", async () => {
|
||||
test("Simple Subflow", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "subflow",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
import { s } from "core/utils/schema";
|
||||
|
||||
describe("Task", async () => {
|
||||
describe.skip("Task", async () => {
|
||||
test("resolveParams: template with parse", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Number()) }),
|
||||
s.object({ test: dynamic(s.number()) }),
|
||||
{
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
@@ -22,7 +22,7 @@ describe("Task", async () => {
|
||||
|
||||
test("resolveParams: with string", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: Type.String() }),
|
||||
s.object({ test: s.string() }),
|
||||
{
|
||||
test: "{{ some.path }}",
|
||||
},
|
||||
@@ -38,7 +38,7 @@ describe("Task", async () => {
|
||||
|
||||
test("resolveParams: with object", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
|
||||
s.object({ test: dynamic(s.object({ key: s.string(), value: s.string() })) }),
|
||||
{
|
||||
test: { key: "path", value: "{{ some.path }}" },
|
||||
},
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { Event, EventManager } from "../../src/core/events";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import { type Static, type StaticDecode, Type } from "@sinclair/typebox";
|
||||
import { s, parse } from "core/utils/schema";
|
||||
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
|
||||
@@ -15,15 +14,15 @@ class Passthrough extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
type OutputIn = Static<typeof OutputParamTask.schema>;
|
||||
type OutputOut = StaticDecode<typeof OutputParamTask.schema>;
|
||||
type OutputIn = s.Static<typeof OutputParamTask.schema>;
|
||||
type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
|
||||
|
||||
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||
type = "output-param";
|
||||
|
||||
static override schema = Type.Object({
|
||||
static override schema = s.strictObject({
|
||||
number: dynamic(
|
||||
Type.Number({
|
||||
s.number({
|
||||
title: "Output number",
|
||||
}),
|
||||
Number.parseInt,
|
||||
@@ -44,7 +43,7 @@ class PassthroughFlowInput extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow task inputs", async () => {
|
||||
describe.skip("Flow task inputs", async () => {
|
||||
test("types", async () => {
|
||||
const schema = OutputParamTask.schema;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class ExecTask extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow trigger", async () => {
|
||||
describe.skip("Flow trigger", async () => {
|
||||
test("manual trigger", async () => {
|
||||
let called = false;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { s } from "core/utils/schema";
|
||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||
|
||||
/*beforeAll(disableConsoleLog);
|
||||
@@ -11,19 +11,19 @@ afterAll(enableConsoleLog);*/
|
||||
class ExecTask extends Task<typeof ExecTask.schema> {
|
||||
type = "exec";
|
||||
|
||||
static override schema = Type.Object({
|
||||
delay: Type.Number({ default: 10 }),
|
||||
static override schema = s.object({
|
||||
delay: s.number({ default: 10 }),
|
||||
});
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
params: Static<typeof ExecTask.schema>,
|
||||
params: s.Static<typeof ExecTask.schema>,
|
||||
private func: () => Promise<any>,
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
|
||||
override clone(name: string, params: Static<typeof ExecTask.schema>) {
|
||||
override clone(name: string, params: s.Static<typeof ExecTask.schema>) {
|
||||
return new ExecTask(name, params, this.func);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
describe("Flow tests", async () => {
|
||||
describe.skip("Flow tests", async () => {
|
||||
test("Simple single task", async () => {
|
||||
const simple = getTask(0);
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ import { unlink } from "node:fs/promises";
|
||||
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
||||
import Database from "libsql";
|
||||
import { format as sqlFormat } from "sql-formatter";
|
||||
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
|
||||
import type { em as protoEm } from "../src/data/prototype";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { slugify } from "core/utils/strings";
|
||||
import { type Connection, SqliteLocalConnection } from "data/connection";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
|
||||
export function getDummyDatabase(memory: boolean = true): {
|
||||
dummyDb: SqliteDatabase;
|
||||
@@ -38,14 +39,15 @@ export function getLocalLibsqlConnection() {
|
||||
return { url: "http://127.0.0.1:8080" };
|
||||
}
|
||||
|
||||
type ConsoleSeverity = "log" | "warn" | "error";
|
||||
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[] = ["log", "warn"]) {
|
||||
export function disableConsoleLog(severities: ConsoleSeverity[] = ["debug", "log", "warn"]) {
|
||||
severities.forEach((severity) => {
|
||||
console[severity] = () => null;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { createApp } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { Api } from "../../src/Api";
|
||||
|
||||
describe("integration config", () => {
|
||||
@@ -13,9 +13,8 @@ describe("integration config", () => {
|
||||
|
||||
// create entity
|
||||
await api.system.addConfig("data", "entities.posts", {
|
||||
name: "posts",
|
||||
config: { sort_field: "id", sort_dir: "asc" },
|
||||
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
|
||||
fields: { id: { type: "primary" }, asdf: { type: "text" } },
|
||||
type: "regular",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/// <reference types="@types/bun" />
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
import { registries } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { mergeObject, randomString } from "../../src/core/utils";
|
||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
@@ -45,7 +46,6 @@ afterAll(enableConsoleLog);
|
||||
describe("MediaController", () => {
|
||||
test("accepts direct", async () => {
|
||||
const app = await makeApp();
|
||||
console.log("app", app);
|
||||
|
||||
const file = Bun.file(path);
|
||||
const name = makeName("png");
|
||||
@@ -54,7 +54,6 @@ describe("MediaController", () => {
|
||||
body: file,
|
||||
});
|
||||
const result = (await res.json()) as any;
|
||||
console.log(result);
|
||||
expect(result.name).toBe(name);
|
||||
|
||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||
import * as StorageEvents from "../../src/media/storage/events";
|
||||
import { StorageAdapter } from "media";
|
||||
import { StorageAdapter } from "media/storage/StorageAdapter";
|
||||
|
||||
class TestAdapter extends StorageAdapter {
|
||||
files: Record<string, FileBody> = {};
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
||||
import { createApp } from "../../src";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { AuthController } from "../../src/auth/api/AuthController";
|
||||
import { em, entity, make, text } from "../../src/data";
|
||||
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||
import { em, entity, make, text } from "data/prototype";
|
||||
import { AppAuth, type ModuleBuildContext } from "modules";
|
||||
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||
// @ts-ignore
|
||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||
|
||||
describe("AppAuth", () => {
|
||||
test.only("...", () => {
|
||||
const auth = new AppAuth({});
|
||||
console.log(auth.toJSON());
|
||||
console.log(auth.config);
|
||||
});
|
||||
|
||||
moduleTestSuite(AppAuth);
|
||||
|
||||
let ctx: ModuleBuildContext;
|
||||
@@ -153,6 +158,7 @@ describe("AppAuth", () => {
|
||||
});
|
||||
|
||||
await app.build();
|
||||
app.registerAdminController();
|
||||
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
|
||||
|
||||
// register custom route
|
||||
@@ -162,6 +168,10 @@ describe("AppAuth", () => {
|
||||
await app.server.request("/api/system/ping");
|
||||
await app.server.request("/test");
|
||||
|
||||
expect(spy.mock.calls.length).toBe(0);
|
||||
|
||||
// admin route
|
||||
await app.server.request("/");
|
||||
expect(spy.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { parse } from "../../src/core/utils";
|
||||
import { parse } from "core/utils/schema";
|
||||
import { fieldsSchema } from "../../src/data/data-schema";
|
||||
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createApp, registries } from "../../src";
|
||||
import { em, entity, text } from "../../src/data";
|
||||
import { createApp } from "core/test/utils";
|
||||
import { em, entity, text } from "data/prototype";
|
||||
import { registries } from "modules/registries";
|
||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||
import { AppMedia } from "../../src/modules";
|
||||
import { AppMedia } from "../../src/media/AppMedia";
|
||||
import { moduleTestSuite } from "./module-test-suite";
|
||||
|
||||
describe("AppMedia", () => {
|
||||
test.only("...", () => {
|
||||
const media = new AppMedia();
|
||||
console.log(media.toJSON());
|
||||
});
|
||||
|
||||
moduleTestSuite(AppMedia);
|
||||
|
||||
test("should allow additional fields", async () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { stripMark } from "../../src/core/utils";
|
||||
import { type TSchema, Type } from "@sinclair/typebox";
|
||||
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||
import { s, stripMark } from "core/utils/schema";
|
||||
import { em, entity, index, text } from "data/prototype";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||
import { Module } from "../../src/modules/Module";
|
||||
import { ModuleHelper } from "modules/ModuleHelper";
|
||||
|
||||
function createModule<Schema extends TSchema>(schema: Schema) {
|
||||
class TestModule extends Module<typeof schema> {
|
||||
function createModule<Schema extends s.Schema>(schema: Schema) {
|
||||
return class TestModule extends Module<Schema> {
|
||||
getSchema() {
|
||||
return schema;
|
||||
}
|
||||
@@ -16,9 +17,7 @@ function createModule<Schema extends TSchema>(schema: Schema) {
|
||||
override useForceParse() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return TestModule;
|
||||
};
|
||||
}
|
||||
|
||||
describe("Module", async () => {
|
||||
@@ -26,7 +25,7 @@ describe("Module", async () => {
|
||||
test("listener", async () => {
|
||||
let result: any;
|
||||
|
||||
const module = createModule(Type.Object({ a: Type.String() }));
|
||||
const module = createModule(s.object({ a: s.string() }));
|
||||
const m = new module({ a: "test" });
|
||||
|
||||
await m.schema().set({ a: "test2" });
|
||||
@@ -42,13 +41,13 @@ describe("Module", async () => {
|
||||
describe("db schema", () => {
|
||||
class M extends Module {
|
||||
override getSchema() {
|
||||
return Type.Object({});
|
||||
return s.object({});
|
||||
}
|
||||
|
||||
prt = {
|
||||
ensureEntity: this.ensureEntity.bind(this),
|
||||
ensureIndex: this.ensureIndex.bind(this),
|
||||
ensureSchema: this.ensureSchema.bind(this),
|
||||
ensureEntity: this.ctx.helper.ensureEntity.bind(this.ctx.helper),
|
||||
ensureIndex: this.ctx.helper.ensureIndex.bind(this.ctx.helper),
|
||||
ensureSchema: this.ctx.helper.ensureSchema.bind(this.ctx.helper),
|
||||
};
|
||||
|
||||
get em() {
|
||||
@@ -63,7 +62,11 @@ describe("Module", async () => {
|
||||
_em.relations,
|
||||
_em.indices,
|
||||
);
|
||||
return new M({} as any, { em, flags: Module.ctx_flags } as any);
|
||||
const ctx = {
|
||||
em,
|
||||
flags: Module.ctx_flags,
|
||||
};
|
||||
return new M({} as any, { ...ctx, helper: new ModuleHelper(ctx as any) } as any);
|
||||
}
|
||||
function flat(_em: EntityManager) {
|
||||
return {
|
||||
@@ -143,14 +146,9 @@ describe("Module", async () => {
|
||||
|
||||
// this should only add the field "important"
|
||||
m.prt.ensureEntity(
|
||||
entity(
|
||||
"u",
|
||||
{
|
||||
important: text(),
|
||||
},
|
||||
undefined,
|
||||
"system",
|
||||
),
|
||||
entity("u", {
|
||||
important: text(),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(m.ctx.flags.sync_required).toBe(true);
|
||||
@@ -159,8 +157,7 @@ describe("Module", async () => {
|
||||
{
|
||||
name: "u",
|
||||
fields: ["id", "name", "important"],
|
||||
// ensured type must be present
|
||||
type: "system",
|
||||
type: "regular",
|
||||
},
|
||||
{
|
||||
name: "p",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import { disableConsoleLog, enableConsoleLog, stripMark } from "core/utils";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { Connection, entity, text } from "data";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
|
||||
import { Module } from "modules/Module";
|
||||
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
||||
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { diff } from "core/object/diff";
|
||||
import type { Static } from "@sinclair/typebox";
|
||||
import { s, stripMark } from "core/utils/schema";
|
||||
import { Connection } from "data/connection/Connection";
|
||||
import { entity, text } from "data/prototype";
|
||||
|
||||
describe("ModuleManager", async () => {
|
||||
test("s1: no config, no build", async () => {
|
||||
@@ -92,7 +92,11 @@ describe("ModuleManager", async () => {
|
||||
|
||||
await mm2.build();
|
||||
|
||||
expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||
/* console.log({
|
||||
json,
|
||||
configs: mm2.configs(),
|
||||
}); */
|
||||
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
|
||||
@@ -257,10 +261,10 @@ describe("ModuleManager", async () => {
|
||||
// @todo: add tests for migrations (check "backup" and new version)
|
||||
|
||||
describe("revert", async () => {
|
||||
const failingModuleSchema = Type.Object({
|
||||
value: Type.Optional(Type.Number()),
|
||||
const failingModuleSchema = s.partialObject({
|
||||
value: s.number(),
|
||||
});
|
||||
class FailingModule extends Module<typeof failingModuleSchema> {
|
||||
class FailingModule extends Module<s.Static<typeof failingModuleSchema>> {
|
||||
getSchema() {
|
||||
return failingModuleSchema;
|
||||
}
|
||||
@@ -431,11 +435,11 @@ describe("ModuleManager", async () => {
|
||||
});
|
||||
|
||||
describe("validate & revert", () => {
|
||||
const schema = Type.Object({
|
||||
value: Type.Array(Type.Number(), { default: [] }),
|
||||
const schema = s.object({
|
||||
value: s.array(s.number()),
|
||||
});
|
||||
type SampleSchema = Static<typeof schema>;
|
||||
class Sample extends Module<typeof schema> {
|
||||
type SampleSchema = s.Static<typeof schema>;
|
||||
class Sample extends Module<SampleSchema> {
|
||||
getSchema() {
|
||||
return schema;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
|
||||
import { Hono } from "hono";
|
||||
import { Guard } from "../../src/auth";
|
||||
import { DebugLogger } from "../../src/core";
|
||||
import { EventManager } from "../../src/core/events";
|
||||
import { Default, stripMark } from "../../src/core/utils";
|
||||
import { EntityManager } from "../../src/data";
|
||||
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
||||
import { Guard } from "auth/authorize/Guard";
|
||||
import { DebugLogger } from "core/utils/DebugLogger";
|
||||
import { EventManager } from "core/events";
|
||||
import { EntityManager } from "data/entities/EntityManager";
|
||||
import { Module, type ModuleBuildContext } from "modules/Module";
|
||||
import { getDummyConnection } from "../helper";
|
||||
import { ModuleHelper } from "modules/ModuleHelper";
|
||||
|
||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||
const { dummyConnection } = getDummyConnection();
|
||||
return {
|
||||
const ctx = {
|
||||
connection: dummyConnection,
|
||||
server: new Hono(),
|
||||
em: new EntityManager([], dummyConnection),
|
||||
@@ -21,6 +21,10 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
||||
logger: new DebugLogger(false),
|
||||
...overrides,
|
||||
};
|
||||
return {
|
||||
...ctx,
|
||||
helper: new ModuleHelper(ctx as any),
|
||||
} as any;
|
||||
}
|
||||
|
||||
export function moduleTestSuite(module: { new (): Module }) {
|
||||
@@ -40,7 +44,8 @@ export function moduleTestSuite(module: { new (): Module }) {
|
||||
it("uses the default config", async () => {
|
||||
const m = new module();
|
||||
await m.setContext(ctx).build();
|
||||
expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
|
||||
expect(m.toJSON()).toEqual(m.getSchema().template({}, { withOptional: true }));
|
||||
//expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+39
-14
@@ -60,7 +60,14 @@ function banner(title: string) {
|
||||
}
|
||||
|
||||
// collection of always-external packages
|
||||
const external = ["bun:test", "node:test", "node:assert/strict", "@libsql/client"] as const;
|
||||
const external = [
|
||||
"bun:test",
|
||||
"node:test",
|
||||
"node:assert/strict",
|
||||
"@libsql/client",
|
||||
"bknd",
|
||||
/^bknd\/.*/,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Building backend and general API
|
||||
@@ -72,13 +79,7 @@ async function buildApi() {
|
||||
sourcemap,
|
||||
watch,
|
||||
define,
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/core/index.ts",
|
||||
"src/core/utils/index.ts",
|
||||
"src/data/index.ts",
|
||||
"src/media/index.ts",
|
||||
],
|
||||
entry: ["src/index.ts", "src/core/utils/index.ts", "src/plugins/index.ts"],
|
||||
outDir: "dist",
|
||||
external: [...external],
|
||||
metafile: true,
|
||||
@@ -225,9 +226,10 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
||||
},
|
||||
external: [
|
||||
/^cloudflare*/,
|
||||
/^@?(hono|libsql).*?/,
|
||||
/^@?hono.*?/,
|
||||
/^(bknd|react|next|node).*?/,
|
||||
/.*\.(html)$/,
|
||||
...external,
|
||||
...(Array.isArray(overrides.external) ? overrides.external : []),
|
||||
],
|
||||
};
|
||||
@@ -244,14 +246,14 @@ async function buildAdapters() {
|
||||
|
||||
// specific adatpers
|
||||
await tsup.build(baseConfig("react-router"));
|
||||
await tsup.build(baseConfig("bun"));
|
||||
await tsup.build(baseConfig("astro"));
|
||||
await tsup.build(baseConfig("aws"));
|
||||
await tsup.build(
|
||||
baseConfig("cloudflare", {
|
||||
external: [/^kysely/],
|
||||
baseConfig("bun", {
|
||||
external: [/^bun\:.*/],
|
||||
}),
|
||||
);
|
||||
await tsup.build(baseConfig("astro"));
|
||||
await tsup.build(baseConfig("aws"));
|
||||
await tsup.build(baseConfig("cloudflare"));
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("vite"),
|
||||
@@ -267,6 +269,29 @@ async function buildAdapters() {
|
||||
...baseConfig("node"),
|
||||
platform: "node",
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("sqlite/edge"),
|
||||
entry: ["src/adapter/sqlite/edge.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("sqlite/node"),
|
||||
entry: ["src/adapter/sqlite/node.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
platform: "node",
|
||||
metafile: false,
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("sqlite/bun"),
|
||||
entry: ["src/adapter/sqlite/bun.ts"],
|
||||
outDir: "dist/adapter/sqlite",
|
||||
metafile: false,
|
||||
external: [/^bun\:.*/],
|
||||
});
|
||||
}
|
||||
|
||||
await buildApi();
|
||||
|
||||
+5
-1
@@ -142,6 +142,7 @@ const adapters = {
|
||||
},
|
||||
nextjs: {
|
||||
dir: path.join(basePath, "examples/nextjs"),
|
||||
env: "TEST_TIMEOUT=20000",
|
||||
clean: async function () {
|
||||
const cwd = path.relative(process.cwd(), this.dir);
|
||||
await $`cd ${cwd} && rm -rf .nextjs data.db`;
|
||||
@@ -195,7 +196,8 @@ async function testAdapter(name: keyof typeof adapters) {
|
||||
console.log("proc:", proc.pid, "data:", c.cyan(data));
|
||||
//proc.kill();process.exit(0);
|
||||
|
||||
await $`TEST_URL=${data} TEST_ADAPTER=${name} bun run test:e2e`;
|
||||
const add_env = "env" in config && config.env ? config.env : "";
|
||||
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
||||
console.log("DONE!");
|
||||
|
||||
while (!proc.killed) {
|
||||
@@ -205,6 +207,8 @@ async function testAdapter(name: keyof typeof adapters) {
|
||||
}
|
||||
}
|
||||
|
||||
// run with: TEST_ADAPTER=astro bun run e2e/adapters.ts
|
||||
// (modify `test:e2e` to `test:e2e:ui` to see the UI)
|
||||
if (process.env.TEST_ADAPTER) {
|
||||
await testAdapter(process.env.TEST_ADAPTER as any);
|
||||
} else {
|
||||
|
||||
+54
-29
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.13.0",
|
||||
"version": "0.16.0-rc.0",
|
||||
"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",
|
||||
"repository": {
|
||||
@@ -13,6 +13,10 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/bknd-io/bknd/issues"
|
||||
},
|
||||
"packageManager": "bun@1.2.19",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||
@@ -31,11 +35,9 @@
|
||||
"test": "ALL_TESTS=1 bun test --bail",
|
||||
"test:all": "bun run test && bun run test:node",
|
||||
"test:bun": "ALL_TESTS=1 bun test --bail",
|
||||
"test:node": "tsx --test $(find . -type f -name '*.native-spec.ts')",
|
||||
"test:node": "vitest run",
|
||||
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||
"test:vitest": "vitest run",
|
||||
"test:vitest:watch": "vitest",
|
||||
"test:vitest:coverage": "vitest run --coverage",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:adapters": "bun run e2e/adapters.ts",
|
||||
@@ -50,10 +52,8 @@
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@hono/swagger-ui": "^0.5.1",
|
||||
"@libsql/client": "^0.15.2",
|
||||
"@mantine/core": "^7.17.1",
|
||||
"@mantine/hooks": "^7.17.1",
|
||||
"@sinclair/typebox": "0.34.30",
|
||||
"@tanstack/react-form": "^1.0.5",
|
||||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
@@ -61,8 +61,7 @@
|
||||
"bcryptjs": "^3.0.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"fast-xml-parser": "^5.0.8",
|
||||
"hono": "^4.7.4",
|
||||
"json-schema-form-react": "^0.0.2",
|
||||
"hono": "4.8.3",
|
||||
"json-schema-library": "10.0.0-rc7",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"kysely": "^0.27.6",
|
||||
@@ -75,15 +74,17 @@
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "^3.758.0",
|
||||
"@bluwy/giget-core": "^0.1.2",
|
||||
"@cloudflare/vitest-pool-workers": "^0.8.38",
|
||||
"@cloudflare/workers-types": "^4.20250606.0",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hono/typebox-validator": "^0.3.2",
|
||||
"@hono/vite-dev-server": "^0.19.0",
|
||||
"@hono/vite-dev-server": "^0.19.1",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@libsql/kysely-libsql": "^0.4.1",
|
||||
"@libsql/client": "^0.15.9",
|
||||
"@mantine/modals": "^7.17.1",
|
||||
"@mantine/notifications": "^7.17.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rjsf/core": "5.22.2",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@tabler/icons-react": "3.18.0",
|
||||
"@tailwindcss/postcss": "^4.0.12",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
@@ -99,8 +100,10 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"jotai": "^2.12.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"jsonv-ts": "^0.0.14-alpha.6",
|
||||
"jsonv-ts": "^0.3.2",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"kysely-generic-sqlite": "^1.2.1",
|
||||
"libsql-stateless-easy": "^1.8.0",
|
||||
"open": "^10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -120,13 +123,15 @@
|
||||
"tsc-alias": "^1.8.11",
|
||||
"tsup": "^8.4.0",
|
||||
"tsx": "^4.19.3",
|
||||
"vite": "^6.2.1",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-circular-dependency": "^0.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.9",
|
||||
"wouter": "^3.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@hono/node-server": "^1.13.8"
|
||||
"@hono/node-server": "^1.14.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19",
|
||||
@@ -156,16 +161,6 @@
|
||||
"import": "./dist/ui/client/index.js",
|
||||
"require": "./dist/ui/client/index.js"
|
||||
},
|
||||
"./data": {
|
||||
"types": "./dist/types/data/index.d.ts",
|
||||
"import": "./dist/data/index.js",
|
||||
"require": "./dist/data/index.js"
|
||||
},
|
||||
"./core": {
|
||||
"types": "./dist/types/core/index.d.ts",
|
||||
"import": "./dist/core/index.js",
|
||||
"require": "./dist/core/index.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/types/core/utils/index.d.ts",
|
||||
"import": "./dist/core/utils/index.js",
|
||||
@@ -176,10 +171,24 @@
|
||||
"import": "./dist/cli/index.js",
|
||||
"require": "./dist/cli/index.js"
|
||||
},
|
||||
"./media": {
|
||||
"types": "./dist/types/media/index.d.ts",
|
||||
"import": "./dist/media/index.js",
|
||||
"require": "./dist/media/index.js"
|
||||
"./plugins": {
|
||||
"types": "./dist/types/plugins/index.d.ts",
|
||||
"import": "./dist/plugins/index.js",
|
||||
"require": "./dist/plugins/index.js"
|
||||
},
|
||||
"./adapter/sqlite": {
|
||||
"types": "./dist/types/adapter/sqlite/edge.d.ts",
|
||||
"import": {
|
||||
"workerd": "./dist/adapter/sqlite/edge.js",
|
||||
"edge-light": "./dist/adapter/sqlite/edge.js",
|
||||
"netlify": "./dist/adapter/sqlite/edge.js",
|
||||
"vercel": "./dist/adapter/sqlite/edge.js",
|
||||
"browser": "./dist/adapter/sqlite/edge.js",
|
||||
"bun": "./dist/adapter/sqlite/bun.js",
|
||||
"node": "./dist/adapter/sqlite/node.js",
|
||||
"default": "./dist/adapter/sqlite/node.js"
|
||||
},
|
||||
"require": "./dist/adapter/sqlite/node.js"
|
||||
},
|
||||
"./adapter/cloudflare": {
|
||||
"types": "./dist/types/adapter/cloudflare/index.d.ts",
|
||||
@@ -227,7 +236,23 @@
|
||||
},
|
||||
"./dist/main.css": "./dist/ui/main.css",
|
||||
"./dist/styles.css": "./dist/ui/styles.css",
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
||||
"./static/*": "./dist/static/*"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"utils": ["./dist/types/core/utils/index.d.ts"],
|
||||
"cli": ["./dist/types/cli/index.d.ts"],
|
||||
"plugins": ["./dist/types/plugins/index.d.ts"],
|
||||
"adapter": ["./dist/types/adapter/index.d.ts"],
|
||||
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
|
||||
"adapter/vite": ["./dist/types/adapter/vite/index.d.ts"],
|
||||
"adapter/nextjs": ["./dist/types/adapter/nextjs/index.d.ts"],
|
||||
"adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
|
||||
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
|
||||
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
|
||||
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
const baseUrl = process.env.TEST_URL || "http://localhost:28623";
|
||||
const startCommand = process.env.TEST_START_COMMAND || "bun run dev";
|
||||
const autoStart = ["1", "true", undefined].includes(process.env.TEST_AUTO_START);
|
||||
const timeout = process.env.TEST_TIMEOUT ? Number.parseInt(process.env.TEST_TIMEOUT) : 5000;
|
||||
|
||||
export default defineConfig({
|
||||
testMatch: "**/*.e2e-spec.ts",
|
||||
@@ -12,7 +13,7 @@ export default defineConfig({
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: "html",
|
||||
timeout: 20000,
|
||||
timeout,
|
||||
use: {
|
||||
baseURL: baseUrl,
|
||||
trace: "on-first-retry",
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { SafeUser } from "auth";
|
||||
import type { SafeUser } from "bknd";
|
||||
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
||||
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
||||
import { decode } from "hono/jwt";
|
||||
|
||||
+99
-50
@@ -1,7 +1,8 @@
|
||||
import type { CreateUserPayload } from "auth/AppAuth";
|
||||
import { $console } from "core";
|
||||
import { $console } from "core/utils";
|
||||
import { Event } from "core/events";
|
||||
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
||||
import type { em as prototypeEm } from "data/prototype";
|
||||
import { Connection } from "data/connection/Connection";
|
||||
import type { Hono } from "hono";
|
||||
import {
|
||||
ModuleManager,
|
||||
@@ -14,15 +15,29 @@ import {
|
||||
import * as SystemPermissions from "modules/permissions";
|
||||
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||
import { SystemController } from "modules/server/SystemController";
|
||||
import type { MaybePromise } from "core/types";
|
||||
import type { ServerEnv } from "modules/Controller";
|
||||
import type { IEmailDriver, ICacheDriver } from "core/drivers";
|
||||
|
||||
// biome-ignore format: must be here
|
||||
import { Api, type ApiOptions } from "Api";
|
||||
import type { ServerEnv } from "modules/Controller";
|
||||
|
||||
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||
export type AppPluginConfig = {
|
||||
name: string;
|
||||
schema?: () => MaybePromise<ReturnType<typeof prototypeEm> | void>;
|
||||
beforeBuild?: () => MaybePromise<void>;
|
||||
onBuilt?: () => MaybePromise<void>;
|
||||
onServerInit?: (server: Hono<ServerEnv>) => MaybePromise<void>;
|
||||
onFirstBoot?: () => MaybePromise<void>;
|
||||
onBoot?: () => MaybePromise<void>;
|
||||
};
|
||||
export type AppPlugin = (app: App) => AppPluginConfig;
|
||||
|
||||
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
|
||||
export class AppConfigUpdatedEvent extends AppEvent {
|
||||
export class AppConfigUpdatedEvent extends AppEvent<{
|
||||
module: string;
|
||||
config: ModuleConfigs[keyof ModuleConfigs];
|
||||
}> {
|
||||
static override slug = "app-config-updated";
|
||||
}
|
||||
export class AppBuiltEvent extends AppEvent {
|
||||
@@ -50,16 +65,13 @@ export type AppOptions = {
|
||||
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||
asyncEventsMode?: "sync" | "async" | "none";
|
||||
drivers?: {
|
||||
email?: IEmailDriver;
|
||||
cache?: ICacheDriver;
|
||||
};
|
||||
};
|
||||
export type CreateAppConfig = {
|
||||
connection?:
|
||||
| Connection
|
||||
| {
|
||||
// @deprecated
|
||||
type: "libsql";
|
||||
config: LibSqlCredentials;
|
||||
}
|
||||
| LibSqlCredentials;
|
||||
connection?: Connection | { url: string };
|
||||
initialConfig?: InitialModuleConfigs;
|
||||
options?: AppOptions;
|
||||
};
|
||||
@@ -67,29 +79,40 @@ export type CreateAppConfig = {
|
||||
export type AppConfig = InitialModuleConfigs;
|
||||
export type LocalApiOptions = Request | ApiOptions;
|
||||
|
||||
export class App {
|
||||
export class App<C extends Connection = Connection, Options extends AppOptions = AppOptions> {
|
||||
static readonly Events = AppEvents;
|
||||
|
||||
modules: ModuleManager;
|
||||
adminController?: AdminController;
|
||||
_id: string = crypto.randomUUID();
|
||||
plugins: Map<string, AppPluginConfig> = new Map();
|
||||
drivers: Options["drivers"] = {};
|
||||
|
||||
private trigger_first_boot = false;
|
||||
private plugins: AppPlugin[];
|
||||
private _building: boolean = false;
|
||||
|
||||
constructor(
|
||||
private connection: Connection,
|
||||
public connection: C,
|
||||
_initialConfig?: InitialModuleConfigs,
|
||||
private options?: AppOptions,
|
||||
private options?: Options,
|
||||
) {
|
||||
this.plugins = options?.plugins ?? [];
|
||||
this.drivers = options?.drivers ?? {};
|
||||
|
||||
for (const plugin of options?.plugins ?? []) {
|
||||
const config = plugin(this);
|
||||
if (this.plugins.has(config.name)) {
|
||||
throw new Error(`Plugin ${config.name} already registered`);
|
||||
}
|
||||
this.plugins.set(config.name, config);
|
||||
}
|
||||
this.runPlugins("onBoot");
|
||||
this.modules = new ModuleManager(connection, {
|
||||
...(options?.manager ?? {}),
|
||||
initial: _initialConfig,
|
||||
onUpdated: this.onUpdated.bind(this),
|
||||
onFirstBoot: this.onFirstBoot.bind(this),
|
||||
onServerInit: this.onServerInit.bind(this),
|
||||
onModulesBuilt: this.onModulesBuilt.bind(this),
|
||||
});
|
||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||
}
|
||||
@@ -98,6 +121,32 @@ export class App {
|
||||
return this.modules.ctx().emgr;
|
||||
}
|
||||
|
||||
protected async runPlugins<Key extends keyof AppPluginConfig>(
|
||||
key: Key,
|
||||
...args: any[]
|
||||
): Promise<{ name: string; result: any }[]> {
|
||||
const results: { name: string; result: any }[] = [];
|
||||
for (const [name, config] of this.plugins) {
|
||||
try {
|
||||
if (key in config && config[key]) {
|
||||
const fn = config[key];
|
||||
if (fn && typeof fn === "function") {
|
||||
$console.debug(`[Plugin:${name}] ${key}`);
|
||||
// @ts-expect-error
|
||||
const result = await fn(...args);
|
||||
results.push({
|
||||
name,
|
||||
result,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$console.warn(`[Plugin:${name}] error running "${key}"`, String(e));
|
||||
}
|
||||
}
|
||||
return results as any;
|
||||
}
|
||||
|
||||
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||
// prevent multiple concurrent builds
|
||||
if (this._building) {
|
||||
@@ -106,6 +155,8 @@ export class App {
|
||||
}
|
||||
if (!options?.forceBuild) return;
|
||||
}
|
||||
|
||||
await this.runPlugins("beforeBuild");
|
||||
this._building = true;
|
||||
|
||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||
@@ -117,13 +168,10 @@ export class App {
|
||||
guard.registerPermissions(Object.values(SystemPermissions));
|
||||
server.route("/api/system", new SystemController(this).getController());
|
||||
|
||||
// load plugins
|
||||
if (this.plugins.length > 0) {
|
||||
await Promise.all(this.plugins.map((plugin) => plugin(this)));
|
||||
}
|
||||
|
||||
// emit built event
|
||||
$console.log("App built");
|
||||
await this.emgr.emit(new AppBuiltEvent({ app: this }));
|
||||
await this.runPlugins("onBuilt");
|
||||
|
||||
// first boot is set from ModuleManager when there wasn't a config table
|
||||
if (this.trigger_first_boot) {
|
||||
@@ -220,15 +268,16 @@ export class App {
|
||||
$console.log("App config updated", module);
|
||||
// @todo: potentially double syncing
|
||||
await this.build({ sync: true });
|
||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this, module, config }));
|
||||
}
|
||||
|
||||
async onFirstBoot() {
|
||||
protected async onFirstBoot() {
|
||||
$console.log("App first boot");
|
||||
this.trigger_first_boot = true;
|
||||
await this.runPlugins("onFirstBoot");
|
||||
}
|
||||
|
||||
async onServerInit(server: Hono<ServerEnv>) {
|
||||
protected async onServerInit(server: Hono<ServerEnv>) {
|
||||
server.use(async (c, next) => {
|
||||
c.set("app", this);
|
||||
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
||||
@@ -253,35 +302,35 @@ export class App {
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// call server init if set
|
||||
if (this.options?.manager?.onServerInit) {
|
||||
this.options.manager.onServerInit(server);
|
||||
}
|
||||
|
||||
await this.runPlugins("onServerInit", server);
|
||||
}
|
||||
|
||||
protected async onModulesBuilt(ctx: ModuleBuildContext) {
|
||||
const results = (await this.runPlugins("schema")) as {
|
||||
name: string;
|
||||
result: ReturnType<typeof prototypeEm>;
|
||||
}[];
|
||||
if (results.length > 0) {
|
||||
for (const { name, result } of results) {
|
||||
if (result) {
|
||||
$console.log(`[Plugin:${name}] schema`);
|
||||
ctx.helper.ensureSchema(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createApp(config: CreateAppConfig = {}) {
|
||||
let connection: Connection | undefined = undefined;
|
||||
|
||||
try {
|
||||
if (Connection.isConnection(config.connection)) {
|
||||
connection = config.connection;
|
||||
} else if (typeof config.connection === "object") {
|
||||
if ("type" in config.connection) {
|
||||
$console.warn(
|
||||
"Using deprecated connection type 'libsql', use the 'config' object directly.",
|
||||
);
|
||||
connection = new LibsqlConnection(config.connection.config);
|
||||
} else {
|
||||
connection = new LibsqlConnection(config.connection);
|
||||
}
|
||||
} else {
|
||||
connection = new LibsqlConnection({ url: ":memory:" });
|
||||
$console.warn("No connection provided, using in-memory database");
|
||||
}
|
||||
} catch (e) {
|
||||
$console.error("Could not create connection", e);
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
if (!config.connection || !Connection.isConnection(config.connection)) {
|
||||
throw new Error("Invalid connection");
|
||||
}
|
||||
|
||||
return new App(connection, config.initialConfig, config.options);
|
||||
return new App(config.connection, config.initialConfig, config.options);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import path from "node:path";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
||||
import { config } from "bknd/core";
|
||||
import { registerLocalMediaAdapter } from ".";
|
||||
import { config, type App } from "bknd";
|
||||
import type { ServeOptions } from "bun";
|
||||
import { serveStatic } from "hono/bun";
|
||||
|
||||
@@ -20,8 +20,8 @@ export async function createApp<Env = BunEnv>(
|
||||
|
||||
return await createRuntimeApp(
|
||||
{
|
||||
...config,
|
||||
serveStatic: serveStatic({ root }),
|
||||
...config,
|
||||
},
|
||||
args ?? (process.env as Env),
|
||||
opts,
|
||||
@@ -33,8 +33,11 @@ export function createHandler<Env = BunEnv>(
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
let app: App | undefined;
|
||||
return async (req: Request) => {
|
||||
const app = await createApp(config, args ?? (process.env as Env), opts);
|
||||
if (!app) {
|
||||
app = await createApp(config, args ?? (process.env as Env), opts);
|
||||
}
|
||||
return app.fetch(req);
|
||||
};
|
||||
}
|
||||
@@ -49,6 +52,7 @@ export function serve<Env = BunEnv>(
|
||||
onBuilt,
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
serveStatic,
|
||||
...serveOptions
|
||||
}: BunBkndConfig<Env> = {},
|
||||
args: Env = {} as Env,
|
||||
@@ -66,11 +70,12 @@ export function serve<Env = BunEnv>(
|
||||
buildConfig,
|
||||
adminOptions,
|
||||
distPath,
|
||||
serveStatic,
|
||||
},
|
||||
args,
|
||||
opts,
|
||||
),
|
||||
});
|
||||
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
console.info(`Server is running on http://localhost:${port}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||
import { bunSqlite } from "./BunSqliteConnection";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { describe } from "bun:test";
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
describe("BunSqliteConnection", () => {
|
||||
connectionTestSuite(bunTestRunner, {
|
||||
makeConnection: () => ({
|
||||
connection: bunSqlite({ database: new Database(":memory:") }),
|
||||
dispose: async () => {},
|
||||
}),
|
||||
rawDialectDetails: [],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||
|
||||
export type BunSqliteConnection = GenericSqliteConnection<Database>;
|
||||
export type BunSqliteConnectionConfig = {
|
||||
database: Database;
|
||||
};
|
||||
|
||||
export function bunSqlite(config?: BunSqliteConnectionConfig | { url: string }) {
|
||||
let db: Database;
|
||||
if (config) {
|
||||
if ("database" in config) {
|
||||
db = config.database;
|
||||
} else {
|
||||
db = new Database(config.url);
|
||||
}
|
||||
} else {
|
||||
db = new Database(":memory:");
|
||||
}
|
||||
|
||||
return genericSqlite("bun-sqlite", db, (utils) => {
|
||||
//const fn = cache ? "query" : "prepare";
|
||||
const getStmt = (sql: string) => db.prepare(sql);
|
||||
|
||||
return {
|
||||
db,
|
||||
query: utils.buildQueryFn({
|
||||
all: (sql, parameters) => getStmt(sql).all(...(parameters || [])),
|
||||
run: (sql, parameters) => {
|
||||
const { changes, lastInsertRowid } = getStmt(sql).run(...(parameters || []));
|
||||
return {
|
||||
insertId: utils.parseBigInt(lastInsertRowid),
|
||||
numAffectedRows: utils.parseBigInt(changes),
|
||||
};
|
||||
},
|
||||
}),
|
||||
close: () => db.close(),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from "./bun.adapter";
|
||||
export * from "../node/storage";
|
||||
export * from "./connection/BunSqliteConnection";
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { expect, test, mock } from "bun:test";
|
||||
import { expect, test, mock, describe, beforeEach, afterEach, afterAll } from "bun:test";
|
||||
|
||||
export const bunTestRunner = {
|
||||
describe,
|
||||
expect,
|
||||
test,
|
||||
mock,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
afterAll,
|
||||
};
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { KyselyPluginRunner, SqliteConnection, SqliteIntrospector } from "bknd/data";
|
||||
import type { QB } from "data/connection/Connection";
|
||||
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
|
||||
import { D1Dialect } from "kysely-d1";
|
||||
|
||||
export type D1ConnectionConfig = {
|
||||
binding: D1Database;
|
||||
};
|
||||
|
||||
class CustomD1Dialect extends D1Dialect {
|
||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db, {
|
||||
excludeTables: ["_cf_KV", "_cf_METADATA"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class D1Connection extends SqliteConnection {
|
||||
protected override readonly supported = {
|
||||
batching: true,
|
||||
};
|
||||
|
||||
constructor(private config: D1ConnectionConfig) {
|
||||
const plugins = [new ParseJSONResultsPlugin()];
|
||||
|
||||
const kysely = new Kysely({
|
||||
dialect: new CustomD1Dialect({ database: config.binding }),
|
||||
plugins,
|
||||
});
|
||||
super(kysely, {}, plugins);
|
||||
}
|
||||
|
||||
get client(): D1Database {
|
||||
return this.config.binding;
|
||||
}
|
||||
|
||||
protected override async batch<Queries extends QB[]>(
|
||||
queries: [...Queries],
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
const db = this.config.binding;
|
||||
|
||||
const res = await db.batch(
|
||||
queries.map((q) => {
|
||||
const { sql, parameters } = q.compile();
|
||||
return db.prepare(sql).bind(...parameters);
|
||||
}),
|
||||
);
|
||||
|
||||
// let it run through plugins
|
||||
const kyselyPlugins = new KyselyPluginRunner(this.plugins);
|
||||
const data: any = [];
|
||||
for (const r of res) {
|
||||
const rows = await kyselyPlugins.transformResultRows(r.results);
|
||||
data.push(rows);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,10 @@ export function getBindings<T extends GetBindingType>(env: any, type: T): Bindin
|
||||
const bindings: BindingMap<T>[] = [];
|
||||
for (const key in env) {
|
||||
try {
|
||||
if (env[key] && (env[key] as any).constructor.name === type) {
|
||||
if (
|
||||
env[key] &&
|
||||
((env[key] as any).constructor.name === type || String(env[key]) === `[object ${type}]`)
|
||||
) {
|
||||
bindings.push({
|
||||
key,
|
||||
value: env[key] as BindingTypeMap[T],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { makeApp } from "./modes/fresh";
|
||||
import { makeConfig } from "./config";
|
||||
import { makeConfig, type CfMakeConfigArgs } from "./config";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
@@ -13,36 +13,38 @@ describe("cf adapter", () => {
|
||||
const DB_URL = ":memory:";
|
||||
const $ctx = (env?: any, request?: Request, ctx?: ExecutionContext) => ({
|
||||
request: request ?? (null as any),
|
||||
env: env ?? { DB_URL },
|
||||
env: env ?? { url: DB_URL },
|
||||
ctx: ctx ?? (null as any),
|
||||
});
|
||||
|
||||
it("makes config", async () => {
|
||||
expect(
|
||||
makeConfig(
|
||||
{
|
||||
connection: { url: DB_URL },
|
||||
},
|
||||
{},
|
||||
),
|
||||
).toEqual({ connection: { url: DB_URL } });
|
||||
const staticConfig = makeConfig(
|
||||
{
|
||||
connection: { url: DB_URL },
|
||||
initialConfig: { data: { basepath: DB_URL } },
|
||||
},
|
||||
$ctx({ DB_URL }),
|
||||
);
|
||||
expect(staticConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
|
||||
expect(staticConfig.connection).toBeDefined();
|
||||
|
||||
expect(
|
||||
makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
connection: { url: env.DB_URL },
|
||||
}),
|
||||
},
|
||||
{
|
||||
DB_URL,
|
||||
},
|
||||
),
|
||||
).toEqual({ connection: { url: DB_URL } });
|
||||
const dynamicConfig = makeConfig(
|
||||
{
|
||||
app: (env) => ({
|
||||
initialConfig: { data: { basepath: env.DB_URL } },
|
||||
connection: { url: env.DB_URL },
|
||||
}),
|
||||
},
|
||||
$ctx({ DB_URL }),
|
||||
);
|
||||
expect(dynamicConfig.initialConfig).toEqual({ data: { basepath: DB_URL } });
|
||||
expect(dynamicConfig.connection).toBeDefined();
|
||||
});
|
||||
|
||||
adapterTestSuite<CloudflareBkndConfig, object>(bunTestRunner, {
|
||||
makeApp,
|
||||
adapterTestSuite<CloudflareBkndConfig, CfMakeConfigArgs<any>>(bunTestRunner, {
|
||||
makeApp: async (c, a, o) => {
|
||||
return await makeApp(c, { env: a } as any, o);
|
||||
},
|
||||
makeHandler: (c, a, o) => {
|
||||
return async (request: any) => {
|
||||
const app = await makeApp(
|
||||
@@ -50,7 +52,7 @@ describe("cf adapter", () => {
|
||||
c ?? {
|
||||
connection: { url: DB_URL },
|
||||
},
|
||||
a,
|
||||
a!,
|
||||
o,
|
||||
);
|
||||
return app.fetch(request);
|
||||
|
||||
@@ -7,9 +7,15 @@ import { getFresh } from "./modes/fresh";
|
||||
import { getCached } from "./modes/cached";
|
||||
import { getDurable } from "./modes/durable";
|
||||
import type { App } from "bknd";
|
||||
import { $console } from "core";
|
||||
import { $console } from "core/utils";
|
||||
|
||||
export type CloudflareEnv = object;
|
||||
declare global {
|
||||
namespace Cloudflare {
|
||||
interface Env {}
|
||||
}
|
||||
}
|
||||
|
||||
export type CloudflareEnv = Cloudflare.Env;
|
||||
export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> & {
|
||||
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||
bindings?: (args: Env) => {
|
||||
@@ -17,11 +23,17 @@ export type CloudflareBkndConfig<Env = CloudflareEnv> = RuntimeBkndConfig<Env> &
|
||||
dobj?: DurableObjectNamespace;
|
||||
db?: D1Database;
|
||||
};
|
||||
d1?: {
|
||||
session?: boolean;
|
||||
transport?: "header" | "cookie";
|
||||
first?: D1SessionConstraint;
|
||||
};
|
||||
static?: "kv" | "assets";
|
||||
key?: string;
|
||||
keepAliveSeconds?: number;
|
||||
forceHttps?: boolean;
|
||||
manifest?: string;
|
||||
registerMedia?: boolean | ((env: Env) => void);
|
||||
};
|
||||
|
||||
export type Context<Env = CloudflareEnv> = {
|
||||
|
||||
@@ -1,50 +1,176 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { Connection } from "bknd";
|
||||
import { sqlite } from "bknd/adapter/sqlite";
|
||||
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
|
||||
import { registerMedia } from "./storage/StorageR2Adapter";
|
||||
import { getBinding } from "./bindings";
|
||||
import { D1Connection } from "./D1Connection";
|
||||
import { d1Sqlite } from "./connection/D1Connection";
|
||||
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
|
||||
import { App } from "bknd";
|
||||
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
|
||||
import type { ExecutionContext } from "hono";
|
||||
import { $console } from "core";
|
||||
import type { Context, ExecutionContext } from "hono";
|
||||
import { $console } from "core/utils";
|
||||
import { setCookie } from "hono/cookie";
|
||||
|
||||
export const constants = {
|
||||
exec_async_event_id: "cf_register_waituntil",
|
||||
cache_endpoint: "/__bknd/cache",
|
||||
do_endpoint: "/__bknd/do",
|
||||
d1_session: {
|
||||
cookie: "cf_d1_session",
|
||||
header: "x-cf-d1-session",
|
||||
},
|
||||
};
|
||||
|
||||
export type CfMakeConfigArgs<Env extends CloudflareEnv = CloudflareEnv> = {
|
||||
env: Env;
|
||||
ctx?: ExecutionContext;
|
||||
request?: Request;
|
||||
};
|
||||
|
||||
function getCookieValue(cookies: string | null, name: string) {
|
||||
if (!cookies) return null;
|
||||
|
||||
for (const cookie of cookies.split("; ")) {
|
||||
const [key, value] = cookie.split("=");
|
||||
if (key === name && value) {
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function d1SessionHelper(config: CloudflareBkndConfig<any>) {
|
||||
const headerKey = constants.d1_session.header;
|
||||
const cookieKey = constants.d1_session.cookie;
|
||||
const transport = config.d1?.transport;
|
||||
|
||||
return {
|
||||
get: (request?: Request): D1SessionBookmark | undefined => {
|
||||
if (!request || !config.d1?.session) return undefined;
|
||||
|
||||
if (!transport || transport === "cookie") {
|
||||
const cookies = request.headers.get("Cookie");
|
||||
if (cookies) {
|
||||
const cookie = getCookieValue(cookies, cookieKey);
|
||||
if (cookie) {
|
||||
return cookie;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!transport || transport === "header") {
|
||||
if (request.headers.has(headerKey)) {
|
||||
return request.headers.get(headerKey) as any;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
set: (c: Context, d1?: D1DatabaseSession) => {
|
||||
if (!d1 || !config.d1?.session) return;
|
||||
|
||||
const session = d1.getBookmark();
|
||||
if (session) {
|
||||
if (!transport || transport === "header") {
|
||||
c.header(headerKey, session);
|
||||
}
|
||||
if (!transport || transport === "cookie") {
|
||||
setCookie(c, cookieKey, session, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "Lax",
|
||||
maxAge: 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let media_registered: boolean = false;
|
||||
export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
args: Env = {} as Env,
|
||||
args?: CfMakeConfigArgs<Env>,
|
||||
) {
|
||||
if (!media_registered) {
|
||||
registerMedia(args as any);
|
||||
if (!media_registered && config.registerMedia !== false) {
|
||||
if (typeof config.registerMedia === "function") {
|
||||
config.registerMedia(args?.env as any);
|
||||
} else {
|
||||
registerMedia(args?.env as any);
|
||||
}
|
||||
media_registered = true;
|
||||
}
|
||||
|
||||
const appConfig = makeAdapterConfig(config, args);
|
||||
const bindings = config.bindings?.(args);
|
||||
if (!appConfig.connection) {
|
||||
const appConfig = makeAdapterConfig(config, args?.env);
|
||||
|
||||
// if connection instance is given, don't do anything
|
||||
// other than checking if D1 session is defined
|
||||
if (Connection.isConnection(appConfig.connection)) {
|
||||
if (config.d1?.session) {
|
||||
// we cannot guarantee that db was opened with session
|
||||
throw new Error(
|
||||
"D1 session don't work when D1 is directly given as connection. Define it in bindings instead.",
|
||||
);
|
||||
}
|
||||
// if connection is given, try to open with unified sqlite adapter
|
||||
} else if (appConfig.connection) {
|
||||
appConfig.connection = sqlite(appConfig.connection);
|
||||
|
||||
// if connection is not given, but env is set
|
||||
// try to make D1 from bindings
|
||||
} else if (args?.env) {
|
||||
const bindings = config.bindings?.(args?.env);
|
||||
const sessionHelper = d1SessionHelper(config);
|
||||
const sessionId = sessionHelper.get(args.request);
|
||||
let session: D1DatabaseSession | undefined;
|
||||
let db: D1Database | undefined;
|
||||
|
||||
// if db is given in bindings, use it
|
||||
if (bindings?.db) {
|
||||
$console.log("Using database from bindings");
|
||||
$console.debug("Using database from bindings");
|
||||
db = bindings.db;
|
||||
} else if (Object.keys(args).length > 0) {
|
||||
const binding = getBinding(args, "D1Database");
|
||||
|
||||
// scan for D1Database in args
|
||||
} else {
|
||||
const binding = getBinding(args.env, "D1Database");
|
||||
if (binding) {
|
||||
$console.log(`Using database from env "${binding.key}"`);
|
||||
$console.debug(`Using database from env "${binding.key}"`);
|
||||
db = binding.value;
|
||||
}
|
||||
}
|
||||
|
||||
// if db is found, check if session is requested
|
||||
if (db) {
|
||||
appConfig.connection = new D1Connection({ binding: db });
|
||||
} else {
|
||||
throw new Error("No database connection given");
|
||||
if (config.d1?.session) {
|
||||
session = db.withSession(sessionId ?? config.d1?.first);
|
||||
if (!session) {
|
||||
throw new Error("Couldn't create session");
|
||||
}
|
||||
|
||||
appConfig.connection = d1Sqlite({ binding: session });
|
||||
appConfig.options = {
|
||||
...appConfig.options,
|
||||
manager: {
|
||||
...appConfig.options?.manager,
|
||||
onServerInit: (server) => {
|
||||
server.use(async (c, next) => {
|
||||
sessionHelper.set(c, session);
|
||||
await next();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
appConfig.connection = d1Sqlite({ binding: db });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Connection.isConnection(appConfig.connection)) {
|
||||
throw new Error("Couldn't find database connection");
|
||||
}
|
||||
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||
import type { QueryResult } from "kysely";
|
||||
|
||||
export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
|
||||
|
||||
export type D1ConnectionConfig<DB extends D1Database | D1DatabaseSession = D1Database> = {
|
||||
binding: DB;
|
||||
};
|
||||
|
||||
export function d1Sqlite<DB extends D1Database | D1DatabaseSession = D1Database>(
|
||||
config: D1ConnectionConfig<DB>,
|
||||
) {
|
||||
const db = config.binding;
|
||||
|
||||
return genericSqlite(
|
||||
"d1-sqlite",
|
||||
db,
|
||||
(utils) => {
|
||||
const getStmt = (sql: string, parameters?: any[] | readonly any[]) =>
|
||||
db.prepare(sql).bind(...(parameters || []));
|
||||
|
||||
const mapResult = (res: D1Result<any>): QueryResult<any> => {
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const numAffectedRows =
|
||||
res.meta.changes > 0 ? utils.parseBigInt(res.meta.changes) : undefined;
|
||||
const insertId = res.meta.last_row_id
|
||||
? utils.parseBigInt(res.meta.last_row_id)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
insertId,
|
||||
numAffectedRows,
|
||||
rows: res.results,
|
||||
// @ts-ignore
|
||||
meta: res.meta,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
db,
|
||||
batch: async (stmts) => {
|
||||
const res = await db.batch(
|
||||
stmts.map(({ sql, parameters }) => {
|
||||
return getStmt(sql, parameters);
|
||||
}),
|
||||
);
|
||||
return res.map(mapResult);
|
||||
},
|
||||
query: utils.buildQueryFn({
|
||||
all: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep.all()).rows;
|
||||
},
|
||||
run: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep.run());
|
||||
},
|
||||
}),
|
||||
close: () => {},
|
||||
};
|
||||
},
|
||||
{
|
||||
supports: {
|
||||
batching: true,
|
||||
softscans: false,
|
||||
},
|
||||
excludeTables: ["_cf_KV", "_cf_METADATA"],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
|
||||
import { viTestRunner } from "adapter/node/vitest";
|
||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||
import { Miniflare } from "miniflare";
|
||||
import { d1Sqlite } from "./D1Connection";
|
||||
|
||||
describe("d1Sqlite", async () => {
|
||||
connectionTestSuite(viTestRunner, {
|
||||
makeConnection: async () => {
|
||||
const mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
d1Databases: ["DB"],
|
||||
});
|
||||
|
||||
const binding = (await mf.getD1Database("DB")) as D1Database;
|
||||
return {
|
||||
connection: d1Sqlite({ binding }),
|
||||
dispose: () => mf.dispose(),
|
||||
};
|
||||
},
|
||||
rawDialectDetails: [
|
||||
"meta.served_by",
|
||||
"meta.duration",
|
||||
"meta.changes",
|
||||
"meta.changed_db",
|
||||
"meta.size_after",
|
||||
"meta.rows_read",
|
||||
"meta.rows_written",
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { genericSqlite, type GenericSqliteConnection } from "bknd";
|
||||
import type { QueryResult } from "kysely";
|
||||
|
||||
export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
|
||||
export type DurableObjecSql = DurableObjectState["storage"]["sql"];
|
||||
|
||||
export type D1ConnectionConfig<DB extends DurableObjecSql> =
|
||||
| DurableObjectState
|
||||
| {
|
||||
sql: DB;
|
||||
};
|
||||
|
||||
export function doSqlite<DB extends DurableObjecSql>(config: D1ConnectionConfig<DB>) {
|
||||
const db = "sql" in config ? config.sql : config.storage.sql;
|
||||
|
||||
return genericSqlite(
|
||||
"do-sqlite",
|
||||
db,
|
||||
(utils) => {
|
||||
// must be async to work with the miniflare mock
|
||||
const getStmt = async (sql: string, parameters?: any[] | readonly any[]) =>
|
||||
await db.exec(sql, ...(parameters || []));
|
||||
|
||||
const mapResult = (
|
||||
cursor: SqlStorageCursor<Record<string, SqlStorageValue>>,
|
||||
): QueryResult<any> => {
|
||||
const numAffectedRows =
|
||||
cursor.rowsWritten > 0 ? utils.parseBigInt(cursor.rowsWritten) : undefined;
|
||||
const insertId = undefined;
|
||||
|
||||
const obj = {
|
||||
insertId,
|
||||
numAffectedRows,
|
||||
rows: cursor.toArray() || [],
|
||||
// @ts-ignore
|
||||
meta: {
|
||||
rowsWritten: cursor.rowsWritten,
|
||||
rowsRead: cursor.rowsRead,
|
||||
databaseSize: db.databaseSize,
|
||||
},
|
||||
};
|
||||
//console.info("mapResult", obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
return {
|
||||
db,
|
||||
batch: async (stmts) => {
|
||||
// @todo: maybe wrap in a transaction?
|
||||
// because d1 implicitly does a transaction on batch
|
||||
return Promise.all(
|
||||
stmts.map(async (stmt) => {
|
||||
return mapResult(await getStmt(stmt.sql, stmt.parameters));
|
||||
}),
|
||||
);
|
||||
},
|
||||
query: utils.buildQueryFn({
|
||||
all: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep).rows;
|
||||
},
|
||||
run: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep);
|
||||
},
|
||||
}),
|
||||
close: () => {},
|
||||
};
|
||||
},
|
||||
{
|
||||
supports: {
|
||||
batching: true,
|
||||
softscans: false,
|
||||
},
|
||||
excludeTables: ["_cf_KV", "_cf_METADATA"],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { describe, test, expect } from "vitest";
|
||||
|
||||
import { viTestRunner } from "adapter/node/vitest";
|
||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||
import { Miniflare } from "miniflare";
|
||||
import { doSqlite } from "./DoConnection";
|
||||
|
||||
const script = `
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
|
||||
export class TestObject extends DurableObject {
|
||||
constructor(ctx, env) {
|
||||
super(ctx, env);
|
||||
this.storage = ctx.storage;
|
||||
}
|
||||
|
||||
async exec(sql, ...parameters) {
|
||||
//return { sql, parameters }
|
||||
const cursor = this.storage.sql.exec(sql, ...parameters);
|
||||
return {
|
||||
rows: cursor.toArray() || [],
|
||||
rowsWritten: cursor.rowsWritten,
|
||||
rowsRead: cursor.rowsRead,
|
||||
databaseSize: this.storage.sql.databaseSize,
|
||||
}
|
||||
}
|
||||
|
||||
async databaseSize() {
|
||||
return this.storage.sql.databaseSize;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const stub = env.TEST_OBJECT.get(env.TEST_OBJECT.idFromName("test"));
|
||||
return stub.fetch(request);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe("doSqlite", async () => {
|
||||
connectionTestSuite(viTestRunner, {
|
||||
makeConnection: async () => {
|
||||
const mf = new Miniflare({
|
||||
modules: true,
|
||||
durableObjects: { TEST_OBJECT: { className: "TestObject", useSQLite: true } },
|
||||
script,
|
||||
});
|
||||
|
||||
const ns = await mf.getDurableObjectNamespace("TEST_OBJECT");
|
||||
const id = ns.idFromName("test");
|
||||
const stub = ns.get(id) as unknown as DurableObjectStub<
|
||||
Rpc.DurableObjectBranded & {
|
||||
exec: (sql: string, ...parameters: any[]) => Promise<any>;
|
||||
}
|
||||
>;
|
||||
|
||||
const stubs: any[] = [];
|
||||
const mock = {
|
||||
databaseSize: 0,
|
||||
exec: async function (sql: string, ...parameters: any[]) {
|
||||
// @ts-ignore
|
||||
const result = (await stub.exec(sql, ...parameters)) as any;
|
||||
this.databaseSize = result.databaseSize;
|
||||
stubs.push(result);
|
||||
return {
|
||||
toArray: () => result.rows,
|
||||
rowsWritten: result.rowsWritten,
|
||||
rowsRead: result.rowsRead,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
connection: doSqlite({ sql: mock as any }),
|
||||
dispose: async () => {
|
||||
await Promise.all(
|
||||
stubs.map((stub) => {
|
||||
try {
|
||||
return stub[Symbol.dispose]();
|
||||
} catch (e) {}
|
||||
}),
|
||||
);
|
||||
await mf.dispose();
|
||||
},
|
||||
};
|
||||
},
|
||||
rawDialectDetails: ["meta.rowsWritten", "meta.rowsRead", "meta.databaseSize"],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ICacheDriver } from "core/drivers";
|
||||
|
||||
interface WorkersKVCacheOptions {
|
||||
// default time-to-live in seconds
|
||||
defaultTTL?: number;
|
||||
// prefix for the cache key
|
||||
cachePrefix?: string;
|
||||
}
|
||||
|
||||
export class WorkersKVCacheDriver implements ICacheDriver {
|
||||
protected readonly kv: KVNamespace;
|
||||
protected readonly defaultTTL?: number;
|
||||
protected readonly cachePrefix: string;
|
||||
|
||||
constructor(kv: KVNamespace, options: WorkersKVCacheOptions = {}) {
|
||||
this.kv = kv;
|
||||
this.cachePrefix = options.cachePrefix ?? "";
|
||||
this.defaultTTL = options.defaultTTL;
|
||||
}
|
||||
|
||||
protected getKey(key: string): string {
|
||||
return this.cachePrefix + key;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
const value = await this.kv.get(this.getKey(key));
|
||||
return value === null ? undefined : value;
|
||||
}
|
||||
|
||||
async set(key: string, value: string, ttl?: number): Promise<void> {
|
||||
let expirationTtl = ttl ?? this.defaultTTL;
|
||||
if (expirationTtl) {
|
||||
expirationTtl = Math.max(expirationTtl, 60);
|
||||
}
|
||||
await this.kv.put(this.getKey(key), value, { expirationTtl: expirationTtl });
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
await this.kv.delete(this.getKey(key));
|
||||
}
|
||||
}
|
||||
|
||||
export const cacheWorkersKV = (kv: KVNamespace, options?: WorkersKVCacheOptions) => {
|
||||
return new WorkersKVCacheDriver(kv, options);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, vi, afterAll, beforeAll } from "vitest";
|
||||
import { cacheWorkersKV } from "./cache";
|
||||
import { viTestRunner } from "adapter/node/vitest";
|
||||
import { cacheDriverTestSuite } from "core/drivers/cache/cache-driver-test-suite";
|
||||
import { Miniflare } from "miniflare";
|
||||
|
||||
describe("cacheWorkersKV", async () => {
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterAll(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
kvNamespaces: ["KV"],
|
||||
});
|
||||
|
||||
const kv = (await mf.getKVNamespace("KV")) as unknown as KVNamespace;
|
||||
|
||||
cacheDriverTestSuite(viTestRunner, {
|
||||
makeCache: () => cacheWorkersKV(kv),
|
||||
setTime: (ms: number) => {
|
||||
vi.advanceTimersByTime(ms);
|
||||
},
|
||||
options: {
|
||||
minTTL: 60,
|
||||
// doesn't work with miniflare
|
||||
skipTTL: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { D1Connection, type D1ConnectionConfig } from "./D1Connection";
|
||||
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
|
||||
|
||||
export * from "./cloudflare-workers.adapter";
|
||||
export { makeApp, getFresh } from "./modes/fresh";
|
||||
export { getCached } from "./modes/cached";
|
||||
export { DurableBkndApp, getDurable } from "./modes/durable";
|
||||
export { D1Connection, type D1ConnectionConfig };
|
||||
export { d1Sqlite, type D1ConnectionConfig };
|
||||
export {
|
||||
getBinding,
|
||||
getBindings,
|
||||
@@ -12,7 +12,13 @@ export {
|
||||
type GetBindingType,
|
||||
type BindingMap,
|
||||
} from "./bindings";
|
||||
export { constants } from "./config";
|
||||
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
|
||||
export { registries } from "bknd";
|
||||
|
||||
export function d1(config: D1ConnectionConfig) {
|
||||
return new D1Connection(config);
|
||||
// for compatibility with old code
|
||||
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
|
||||
config: D1ConnectionConfig<DB>,
|
||||
) {
|
||||
return d1Sqlite<DB>(config);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import { makeConfig, registerAsyncsExecutionContext, constants } from "../config
|
||||
|
||||
export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
{ env, ctx, ...args }: Context<Env>,
|
||||
args: Context<Env>,
|
||||
) {
|
||||
const { env, ctx } = args;
|
||||
const { kv } = config.bindings?.(env)!;
|
||||
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||
const key = config.key ?? "app";
|
||||
@@ -20,7 +21,7 @@ export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
|
||||
const app = await createRuntimeApp(
|
||||
{
|
||||
...makeConfig(config, env),
|
||||
...makeConfig(config, args),
|
||||
initialConfig,
|
||||
onBuilt: async (app) => {
|
||||
registerAsyncsExecutionContext(app, ctx);
|
||||
@@ -41,7 +42,7 @@ export async function getCached<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
await config.beforeBuild?.(app);
|
||||
},
|
||||
},
|
||||
{ env, ctx, ...args },
|
||||
args,
|
||||
);
|
||||
|
||||
if (!cachedConfig) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { App, CreateAppConfig } from "bknd";
|
||||
import { createRuntimeApp, makeConfig } from "bknd/adapter";
|
||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||
import { constants, registerAsyncsExecutionContext } from "../config";
|
||||
import { $console } from "core";
|
||||
import { $console } from "core/utils";
|
||||
|
||||
export async function getDurable<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
@@ -64,7 +64,7 @@ export class DurableBkndApp extends DurableObject {
|
||||
"type" in config.connection &&
|
||||
config.connection.type === "libsql"
|
||||
) {
|
||||
config.connection.config.protocol = "wss";
|
||||
//config.connection.config.protocol = "wss";
|
||||
}
|
||||
|
||||
this.app = await createRuntimeApp({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
|
||||
import { makeConfig, registerAsyncsExecutionContext } from "../config";
|
||||
import { makeConfig, registerAsyncsExecutionContext, type CfMakeConfigArgs } from "../config";
|
||||
|
||||
export async function makeApp<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
config: CloudflareBkndConfig<Env>,
|
||||
args: Env = {} as Env,
|
||||
args?: CfMakeConfigArgs<Env>,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
return await createRuntimeApp<Env>(makeConfig(config, args), args, opts);
|
||||
return await createRuntimeApp<Env>(makeConfig(config, args), args?.env, opts);
|
||||
}
|
||||
|
||||
export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
@@ -23,7 +23,7 @@ export async function getFresh<Env extends CloudflareEnv = CloudflareEnv>(
|
||||
await config.onBuilt?.(app);
|
||||
},
|
||||
},
|
||||
ctx.env,
|
||||
ctx,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createWriteStream, readFileSync } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
import { Miniflare } from "miniflare";
|
||||
import { StorageR2Adapter } from "./StorageR2Adapter";
|
||||
import { adapterTestSuite } from "media";
|
||||
import { nodeTestRunner } from "adapter/node/test";
|
||||
import path from "node:path";
|
||||
|
||||
// https://github.com/nodejs/node/issues/44372#issuecomment-1736530480
|
||||
console.log = async (message: any) => {
|
||||
const tty = createWriteStream("/dev/tty");
|
||||
const msg = typeof message === "string" ? message : JSON.stringify(message, null, 2);
|
||||
return tty.write(`${msg}\n`);
|
||||
};
|
||||
|
||||
test("StorageR2Adapter", async () => {
|
||||
const mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
r2Buckets: ["BUCKET"],
|
||||
});
|
||||
|
||||
const bucket = (await mf.getR2Bucket("BUCKET")) as unknown as R2Bucket;
|
||||
const adapter = new StorageR2Adapter(bucket);
|
||||
|
||||
const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets");
|
||||
const buffer = readFileSync(path.join(basePath, "image.png"));
|
||||
const file = new File([buffer], "image.png", { type: "image/png" });
|
||||
|
||||
await adapterTestSuite(nodeTestRunner, adapter, file);
|
||||
await mf.dispose();
|
||||
});
|
||||
@@ -1,15 +1,12 @@
|
||||
import { registries } from "bknd";
|
||||
import { isDebug } from "bknd/core";
|
||||
import { StringEnum } from "bknd/utils";
|
||||
import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media";
|
||||
import { registries, isDebug, guessMimeType } from "bknd";
|
||||
import { getBindings } from "../bindings";
|
||||
import * as tb from "@sinclair/typebox";
|
||||
const { Type } = tb;
|
||||
import { s } from "bknd/utils";
|
||||
import { StorageAdapter, type FileBody } from "bknd";
|
||||
|
||||
export function makeSchema(bindings: string[] = []) {
|
||||
return Type.Object(
|
||||
return s.object(
|
||||
{
|
||||
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String()),
|
||||
binding: bindings.length > 0 ? s.string({ enum: bindings }) : s.string().optional(),
|
||||
},
|
||||
{ title: "R2", description: "Cloudflare R2 storage" },
|
||||
);
|
||||
@@ -63,46 +60,49 @@ export class StorageR2Adapter extends StorageAdapter {
|
||||
|
||||
async putObject(key: string, body: FileBody) {
|
||||
try {
|
||||
const res = await this.bucket.put(key, body);
|
||||
const res = await this.bucket.put(this.getKey(key), body);
|
||||
return res?.etag;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
async listObjects(
|
||||
prefix?: string,
|
||||
): Promise<{ key: string; last_modified: Date; size: number }[]> {
|
||||
const list = await this.bucket.list({ limit: 50 });
|
||||
async listObjects(prefix = ""): Promise<{ key: string; last_modified: Date; size: number }[]> {
|
||||
const list = await this.bucket.list({ limit: 50, prefix: this.getKey(prefix) });
|
||||
return list.objects.map((item) => ({
|
||||
key: item.key,
|
||||
key: item.key.replace(this.getKey(""), ""),
|
||||
size: item.size,
|
||||
last_modified: item.uploaded,
|
||||
}));
|
||||
}
|
||||
|
||||
private async headObject(key: string): Promise<R2Object | null> {
|
||||
return await this.bucket.head(key);
|
||||
return await this.bucket.head(this.getKey(key));
|
||||
}
|
||||
|
||||
async objectExists(key: string): Promise<boolean> {
|
||||
return (await this.headObject(key)) !== null;
|
||||
}
|
||||
|
||||
async getObject(key: string, headers: Headers): Promise<Response> {
|
||||
async getObject(_key: string, headers: Headers): Promise<Response> {
|
||||
let object: R2ObjectBody | null;
|
||||
const key = this.getKey(_key);
|
||||
|
||||
const responseHeaders = new Headers({
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Type": guess(key),
|
||||
"Content-Type": guessMimeType(key),
|
||||
});
|
||||
|
||||
const range = headers.has("range");
|
||||
|
||||
//console.log("getObject:headers", headersToObject(headers));
|
||||
if (headers.has("range")) {
|
||||
if (range) {
|
||||
const options = isDebug()
|
||||
? {} // miniflare doesn't support range requests
|
||||
: {
|
||||
range: headers,
|
||||
onlyIf: headers,
|
||||
};
|
||||
|
||||
object = (await this.bucket.get(key, options)) as R2ObjectBody;
|
||||
|
||||
if (!object) {
|
||||
@@ -130,17 +130,18 @@ export class StorageR2Adapter extends StorageAdapter {
|
||||
responseHeaders.set("Last-Modified", object.uploaded.toUTCString());
|
||||
|
||||
return new Response(object.body, {
|
||||
status: object.range ? 206 : 200,
|
||||
status: range ? 206 : 200,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
private writeHttpMetadata(headers: Headers, object: R2Object | R2ObjectBody): void {
|
||||
let metadata = object.httpMetadata;
|
||||
|
||||
if (!metadata || Object.keys(metadata).length === 0) {
|
||||
// guessing is especially required for dev environment (miniflare)
|
||||
metadata = {
|
||||
contentType: guess(object.key),
|
||||
contentType: guessMimeType(object.key),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,19 +158,23 @@ export class StorageR2Adapter extends StorageAdapter {
|
||||
}
|
||||
|
||||
return {
|
||||
type: String(head.httpMetadata?.contentType ?? guess(key)),
|
||||
type: String(head.httpMetadata?.contentType ?? guessMimeType(key)),
|
||||
size: head.size,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteObject(key: string): Promise<void> {
|
||||
await this.bucket.delete(key);
|
||||
await this.bucket.delete(this.getKey(key));
|
||||
}
|
||||
|
||||
getObjectUrl(key: string): string {
|
||||
throw new Error("Method getObjectUrl not implemented.");
|
||||
}
|
||||
|
||||
protected getKey(key: string) {
|
||||
return key;
|
||||
}
|
||||
|
||||
toJSON(secrets?: boolean) {
|
||||
return {
|
||||
type: this.getName(),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { Miniflare } from "miniflare";
|
||||
import { StorageR2Adapter } from "./StorageR2Adapter";
|
||||
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
||||
import path from "node:path";
|
||||
import { describe, afterAll, test, expect } from "vitest";
|
||||
import { viTestRunner } from "adapter/node/vitest";
|
||||
|
||||
let mf: Miniflare | undefined;
|
||||
describe("StorageR2Adapter", async () => {
|
||||
mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
r2Buckets: ["BUCKET"],
|
||||
});
|
||||
const bucket = (await mf?.getR2Bucket("BUCKET")) as unknown as R2Bucket;
|
||||
|
||||
test("test", () => {
|
||||
expect(bucket).toBeDefined();
|
||||
});
|
||||
const adapter = new StorageR2Adapter(bucket);
|
||||
|
||||
const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets");
|
||||
const buffer = readFileSync(path.join(basePath, "image.png"));
|
||||
const file = new File([buffer], "image.png", { type: "image/png" });
|
||||
|
||||
await adapterTestSuite(viTestRunner, adapter, file);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mf?.dispose();
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
|
||||
|
||||
export default defineWorkersConfig({
|
||||
test: {
|
||||
poolOptions: {
|
||||
workers: {
|
||||
miniflare: {
|
||||
compatibilityDate: "2025-06-04",
|
||||
},
|
||||
},
|
||||
},
|
||||
include: ["**/*.vi-test.ts", "**/*.vitest.ts"],
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { App, type CreateAppConfig } from "bknd";
|
||||
import { config as $config } from "bknd/core";
|
||||
import type { MiddlewareHandler } from "hono";
|
||||
import { config as $config, App, type CreateAppConfig, Connection, guessMimeType } from "bknd";
|
||||
import { $console } from "bknd/utils";
|
||||
import type { Context, MiddlewareHandler, Next } from "hono";
|
||||
import type { AdminControllerOptions } from "modules/server/AdminController";
|
||||
import type { Manifest } from "vite";
|
||||
|
||||
export type BkndConfig<Args = any> = CreateAppConfig & {
|
||||
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
|
||||
@@ -59,7 +60,21 @@ export async function createAdapterApp<Config extends BkndConfig = BkndConfig, A
|
||||
const id = opts?.id ?? "app";
|
||||
let app = apps.get(id);
|
||||
if (!app || opts?.force) {
|
||||
app = App.create(makeConfig(config, args));
|
||||
const appConfig = 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);
|
||||
$console.info(`Using ${connection!.name} connection`, conf.url);
|
||||
}
|
||||
appConfig.connection = connection;
|
||||
}
|
||||
|
||||
app = App.create(appConfig);
|
||||
apps.set(id, app);
|
||||
}
|
||||
return app;
|
||||
@@ -122,3 +137,55 @@ export async function createRuntimeApp<Args = DefaultArgs>(
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a middleware handler to serve static assets via dynamic imports.
|
||||
* This is useful for environments where filesystem access is limited but bundled assets can be imported.
|
||||
*
|
||||
* @param manifest - Vite manifest object containing asset information
|
||||
* @returns Hono middleware handler for serving static assets
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { serveStaticViaImport } from "bknd/adapter";
|
||||
*
|
||||
* serve({
|
||||
* serveStatic: serveStaticViaImport(),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function serveStaticViaImport(opts?: { manifest?: Manifest }) {
|
||||
let files: string[] | undefined;
|
||||
|
||||
// @ts-ignore
|
||||
return async (c: Context, next: Next) => {
|
||||
if (!files) {
|
||||
const manifest =
|
||||
opts?.manifest || ((await import("bknd/dist/manifest.json")).default as Manifest);
|
||||
files = Object.values(manifest).flatMap((asset) => [asset.file, ...(asset.css || [])]);
|
||||
}
|
||||
|
||||
const path = c.req.path.substring(1);
|
||||
if (files.includes(path)) {
|
||||
try {
|
||||
const content = await import(`bknd/static/${path}?raw`, {
|
||||
/* @vite-ignore */
|
||||
assert: { type: "text" },
|
||||
}).then((m) => m.default);
|
||||
|
||||
if (content) {
|
||||
return c.body(content, {
|
||||
headers: {
|
||||
"Content-Type": guessMimeType(path),
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error serving static file:", e);
|
||||
return c.text("File not found", 404);
|
||||
}
|
||||
}
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { isNode } from "bknd/utils";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
type NextjsEnv = NextApiRequest["env"];
|
||||
|
||||
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
|
||||
cleanRequest?: { searchParams?: string[] };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { genericSqlite } from "bknd";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
export type NodeSqliteConnectionConfig = {
|
||||
database: DatabaseSync;
|
||||
};
|
||||
|
||||
export function nodeSqlite(config?: NodeSqliteConnectionConfig | { url: string }) {
|
||||
let db: DatabaseSync;
|
||||
if (config) {
|
||||
if ("database" in config) {
|
||||
db = config.database;
|
||||
} else {
|
||||
db = new DatabaseSync(config.url);
|
||||
}
|
||||
} else {
|
||||
db = new DatabaseSync(":memory:");
|
||||
}
|
||||
|
||||
return genericSqlite(
|
||||
"node-sqlite",
|
||||
db,
|
||||
(utils) => {
|
||||
const getStmt = (sql: string) => {
|
||||
const stmt = db.prepare(sql);
|
||||
//stmt.setReadBigInts(true);
|
||||
return stmt;
|
||||
};
|
||||
|
||||
return {
|
||||
db,
|
||||
query: utils.buildQueryFn({
|
||||
all: (sql, parameters = []) => getStmt(sql).all(...parameters),
|
||||
run: (sql, parameters = []) => {
|
||||
const { changes, lastInsertRowid } = getStmt(sql).run(...parameters);
|
||||
return {
|
||||
insertId: utils.parseBigInt(lastInsertRowid),
|
||||
numAffectedRows: utils.parseBigInt(changes),
|
||||
};
|
||||
},
|
||||
}),
|
||||
close: () => db.close(),
|
||||
iterator: (isSelect, sql, parameters = []) => {
|
||||
if (!isSelect) {
|
||||
throw new Error("Only support select in stream()");
|
||||
}
|
||||
return getStmt(sql).iterate(...parameters) as any;
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
supports: {
|
||||
batching: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { nodeSqlite } from "./NodeSqliteConnection";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||
import { describe } from "vitest";
|
||||
import { viTestRunner } from "../vitest";
|
||||
|
||||
describe("NodeSqliteConnection", () => {
|
||||
connectionTestSuite(viTestRunner, {
|
||||
makeConnection: () => ({
|
||||
connection: nodeSqlite({ database: new DatabaseSync(":memory:") }),
|
||||
dispose: async () => {},
|
||||
}),
|
||||
rawDialectDetails: [],
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,3 @@
|
||||
import { registries } from "bknd";
|
||||
import { type LocalAdapterConfig, StorageLocalAdapter } from "./storage/StorageLocalAdapter";
|
||||
|
||||
export * from "./node.adapter";
|
||||
export { StorageLocalAdapter, type LocalAdapterConfig };
|
||||
|
||||
let registered = false;
|
||||
export function registerLocalMediaAdapter() {
|
||||
if (!registered) {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
registered = true;
|
||||
}
|
||||
|
||||
return (config: Partial<LocalAdapterConfig> = {}) => {
|
||||
const adapter = new StorageLocalAdapter(config);
|
||||
return adapter.toJSON(true);
|
||||
};
|
||||
}
|
||||
export * from "./storage";
|
||||
export * from "./connection/NodeSqliteConnection";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, beforeAll, describe } from "bun:test";
|
||||
import * as node from "./node.adapter";
|
||||
import { createApp, createHandler } from "./node.adapter";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { bunTestRunner } from "adapter/bun/test";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
@@ -9,7 +9,7 @@ afterAll(enableConsoleLog);
|
||||
|
||||
describe("node adapter (bun)", () => {
|
||||
adapterTestSuite(bunTestRunner, {
|
||||
makeApp: node.createApp,
|
||||
makeHandler: node.createHandler,
|
||||
makeApp: createApp,
|
||||
makeHandler: createHandler,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import path from "node:path";
|
||||
import { serve as honoServe } from "@hono/node-server";
|
||||
import { serveStatic } from "@hono/node-server/serve-static";
|
||||
import { registerLocalMediaAdapter } from "adapter/node/index";
|
||||
import { registerLocalMediaAdapter } from "adapter/node/storage";
|
||||
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
|
||||
import { config as $config } from "bknd/core";
|
||||
import { $console } from "core";
|
||||
import { config as $config, type App } from "bknd";
|
||||
import { $console } from "bknd/utils";
|
||||
|
||||
type NodeEnv = NodeJS.ProcessEnv;
|
||||
export type NodeBkndConfig<Env = NodeEnv> = RuntimeBkndConfig<Env> & {
|
||||
@@ -31,8 +31,8 @@ export async function createApp<Env = NodeEnv>(
|
||||
registerLocalMediaAdapter();
|
||||
return await createRuntimeApp(
|
||||
{
|
||||
...config,
|
||||
serveStatic: serveStatic({ root }),
|
||||
...config,
|
||||
},
|
||||
// @ts-ignore
|
||||
args ?? { env: process.env },
|
||||
@@ -45,8 +45,11 @@ export function createHandler<Env = NodeEnv>(
|
||||
args: Env = {} as Env,
|
||||
opts?: RuntimeOptions,
|
||||
) {
|
||||
let app: App | undefined;
|
||||
return async (req: Request) => {
|
||||
const app = await createApp(config, args ?? (process.env as Env), opts);
|
||||
if (!app) {
|
||||
app = await createApp(config, args ?? (process.env as Env), opts);
|
||||
}
|
||||
return app.fetch(req);
|
||||
};
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,14 +1,14 @@
|
||||
import { describe, before, after } from "node:test";
|
||||
import { describe, beforeAll, afterAll } from "vitest";
|
||||
import * as node from "./node.adapter";
|
||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||
import { nodeTestRunner } from "adapter/node/test";
|
||||
import { viTestRunner } from "adapter/node/vitest";
|
||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||
|
||||
before(() => disableConsoleLog());
|
||||
after(enableConsoleLog);
|
||||
beforeAll(() => disableConsoleLog());
|
||||
afterAll(enableConsoleLog);
|
||||
|
||||
describe("node adapter", () => {
|
||||
adapterTestSuite(nodeTestRunner, {
|
||||
adapterTestSuite(viTestRunner, {
|
||||
makeApp: node.createApp,
|
||||
makeHandler: node.createHandler,
|
||||
});
|
||||
@@ -1,17 +1,15 @@
|
||||
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
||||
import { type Static, isFile, parse } from "bknd/utils";
|
||||
import type { FileBody, FileListObject, FileMeta, FileUploadPayload } from "bknd/media";
|
||||
import { StorageAdapter, guessMimeType as guess } from "bknd/media";
|
||||
import * as tb from "@sinclair/typebox";
|
||||
const { Type } = tb;
|
||||
import type { FileBody, FileListObject, FileMeta, FileUploadPayload } from "bknd";
|
||||
import { StorageAdapter, guessMimeType } from "bknd";
|
||||
import { parse, s, isFile } from "bknd/utils";
|
||||
|
||||
export const localAdapterConfig = Type.Object(
|
||||
export const localAdapterConfig = s.object(
|
||||
{
|
||||
path: Type.String({ default: "./" }),
|
||||
path: s.string({ default: "./" }),
|
||||
},
|
||||
{ title: "Local", description: "Local file system storage", additionalProperties: false },
|
||||
);
|
||||
export type LocalAdapterConfig = Static<typeof localAdapterConfig>;
|
||||
export type LocalAdapterConfig = s.Static<typeof localAdapterConfig>;
|
||||
|
||||
export class StorageLocalAdapter extends StorageAdapter {
|
||||
private config: LocalAdapterConfig;
|
||||
@@ -62,8 +60,7 @@ export class StorageLocalAdapter extends StorageAdapter {
|
||||
}
|
||||
|
||||
const filePath = `${this.config.path}/${key}`;
|
||||
const is_file = isFile(body);
|
||||
await writeFile(filePath, is_file ? body.stream() : body);
|
||||
await writeFile(filePath, isFile(body) ? body.stream() : body);
|
||||
|
||||
return await this.computeEtag(body);
|
||||
}
|
||||
@@ -86,7 +83,7 @@ export class StorageLocalAdapter extends StorageAdapter {
|
||||
async getObject(key: string, headers: Headers): Promise<Response> {
|
||||
try {
|
||||
const content = await readFile(`${this.config.path}/${key}`);
|
||||
const mimeType = guess(key);
|
||||
const mimeType = guessMimeType(key);
|
||||
|
||||
return new Response(content, {
|
||||
status: 200,
|
||||
@@ -108,7 +105,7 @@ export class StorageLocalAdapter extends StorageAdapter {
|
||||
async getObjectMeta(key: string): Promise<FileMeta> {
|
||||
const stats = await stat(`${this.config.path}/${key}`);
|
||||
return {
|
||||
type: guess(key) || "application/octet-stream",
|
||||
type: guessMimeType(key) || "application/octet-stream",
|
||||
size: stats.size,
|
||||
};
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { describe } from "node:test";
|
||||
import { nodeTestRunner } from "adapter/node/test";
|
||||
import { describe } from "vitest";
|
||||
import { viTestRunner } from "adapter/node/vitest";
|
||||
import { StorageLocalAdapter } from "adapter/node";
|
||||
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -14,5 +14,5 @@ describe("StorageLocalAdapter (node)", async () => {
|
||||
path: path.join(basePath, "tmp"),
|
||||
});
|
||||
|
||||
await adapterTestSuite(nodeTestRunner, adapter, file);
|
||||
await adapterTestSuite(viTestRunner, adapter, file);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { registries } from "bknd";
|
||||
import { type LocalAdapterConfig, StorageLocalAdapter } from "./StorageLocalAdapter";
|
||||
|
||||
export * from "./StorageLocalAdapter";
|
||||
|
||||
let registered = false;
|
||||
export function registerLocalMediaAdapter() {
|
||||
if (!registered) {
|
||||
registries.media.register("local", StorageLocalAdapter);
|
||||
registered = true;
|
||||
}
|
||||
|
||||
return (config: Partial<LocalAdapterConfig> = {}) => {
|
||||
const adapter = new StorageLocalAdapter(config);
|
||||
return adapter.toJSON(true);
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user