mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
Merge remote-tracking branch 'origin/release/0.20' into feat/opfs-and-sqlocal
This commit is contained in:
19
.github/workflows/test.yml
vendored
19
.github/workflows/test.yml
vendored
@@ -9,6 +9,21 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: bknd
|
||||||
|
ports:
|
||||||
|
- 5430:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -20,11 +35,11 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: "1.3.2"
|
bun-version: "1.3.3"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: bun install
|
run: bun install #--linker=hoisted
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe("MediaApi", () => {
|
|||||||
const res = await mockedBackend.request("/api/media/file/" + name);
|
const res = await mockedBackend.request("/api/media/file/" + name);
|
||||||
await Bun.write(path, res);
|
await Bun.write(path, res);
|
||||||
|
|
||||||
const file = await Bun.file(path);
|
const file = Bun.file(path);
|
||||||
expect(file.size).toBeGreaterThan(0);
|
expect(file.size).toBeGreaterThan(0);
|
||||||
expect(file.type).toBe("image/png");
|
expect(file.type).toBe("image/png");
|
||||||
await file.delete();
|
await file.delete();
|
||||||
@@ -154,15 +154,12 @@ describe("MediaApi", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// upload via readable from bun
|
// upload via readable from bun
|
||||||
await matches(await api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
await matches(api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
||||||
|
|
||||||
// upload via readable from response
|
// upload via readable from response
|
||||||
{
|
{
|
||||||
const response = (await mockedBackend.request(url)) as Response;
|
const response = (await mockedBackend.request(url)) as Response;
|
||||||
await matches(
|
await matches(api.upload(response.body!, { filename: "readable.png" }), "readable.png");
|
||||||
await api.upload(response.body!, { filename: "readable.png" }),
|
|
||||||
"readable.png",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { describe, expect, mock, test } from "bun:test";
|
import { describe, expect, mock, test, beforeAll, afterAll } from "bun:test";
|
||||||
import { createApp as internalCreateApp, type CreateAppConfig } from "bknd";
|
import { createApp as internalCreateApp, type CreateAppConfig } from "bknd";
|
||||||
import { getDummyConnection } from "../../__test__/helper";
|
import { getDummyConnection } from "../../__test__/helper";
|
||||||
import { ModuleManager } from "modules/ModuleManager";
|
import { ModuleManager } from "modules/ModuleManager";
|
||||||
import { em, entity, text } from "data/prototype";
|
import { em, entity, text } from "data/prototype";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
async function createApp(config: CreateAppConfig = {}) {
|
async function createApp(config: CreateAppConfig = {}) {
|
||||||
const app = internalCreateApp({
|
const app = internalCreateApp({
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { AppEvents } from "App";
|
import { AppEvents } from "App";
|
||||||
import { describe, test, expect, beforeAll, mock } from "bun:test";
|
import { describe, test, expect, beforeAll, mock, afterAll } from "bun:test";
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
||||||
import type { McpServer } from "bknd/utils";
|
import type { McpServer } from "bknd/utils";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - [x] system_config
|
* - [x] system_config
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
||||||
import { Guard, type GuardConfig } from "auth/authorize/Guard";
|
import { Guard, type GuardConfig } from "auth/authorize/Guard";
|
||||||
import { Permission } from "auth/authorize/Permission";
|
import { Permission } from "auth/authorize/Permission";
|
||||||
import { Role, type RoleSchema } from "auth/authorize/Role";
|
import { Role, type RoleSchema } from "auth/authorize/Role";
|
||||||
import { objectTransform, s } from "bknd/utils";
|
import { objectTransform, s } from "bknd/utils";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
function createGuard(
|
function createGuard(
|
||||||
permissionNames: string[],
|
permissionNames: string[],
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import type { App, DB } from "bknd";
|
|||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
import type { CreateUserPayload } from "auth/AppAuth";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(() => enableConsoleLog());
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
async function makeApp(config: Partial<CreateAppConfig["config"]> = {}) {
|
async function makeApp(config: Partial<CreateAppConfig["config"]> = {}) {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
|
|||||||
40
app/__test__/auth/authorize/http/DataController.test.ts
Normal file
40
app/__test__/auth/authorize/http/DataController.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||||
|
import { createAuthTestApp } from "./shared";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
import { em, entity, text } from "data/prototype";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
const schema = em(
|
||||||
|
{
|
||||||
|
posts: entity("posts", {
|
||||||
|
title: text(),
|
||||||
|
content: text(),
|
||||||
|
}),
|
||||||
|
comments: entity("comments", {
|
||||||
|
content: text(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
({ relation }, { posts, comments }) => {
|
||||||
|
relation(posts).manyToOne(comments);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("DataController (auth)", () => {
|
||||||
|
test("reading schema.json", async () => {
|
||||||
|
const { request } = await createAuthTestApp(
|
||||||
|
{
|
||||||
|
permission: ["system.access.api", "data.entity.read", "system.schema.read"],
|
||||||
|
request: new Request("http://localhost/api/data/schema.json"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: { data: schema.toJSON() },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect((await request.guest()).status).toBe(403);
|
||||||
|
expect((await request.member()).status).toBe(403);
|
||||||
|
expect((await request.authorized()).status).toBe(200);
|
||||||
|
expect((await request.admin()).status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import { SystemController } from "modules/server/SystemController";
|
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import type { CreateAppConfig } from "App";
|
|
||||||
import { getPermissionRoutes } from "auth/middlewares/permission.middleware";
|
|
||||||
|
|
||||||
async function makeApp(config: Partial<CreateAppConfig> = {}) {
|
|
||||||
const app = createApp(config);
|
|
||||||
await app.build();
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe.skip("SystemController", () => {
|
|
||||||
it("...", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const controller = new SystemController(app);
|
|
||||||
const hono = controller.getController();
|
|
||||||
console.log(getPermissionRoutes(hono));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
41
app/__test__/auth/authorize/http/SystemController.test.ts
Normal file
41
app/__test__/auth/authorize/http/SystemController.test.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
||||||
|
import { createAuthTestApp } from "./shared";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("SystemController (auth)", () => {
|
||||||
|
test("reading info", async () => {
|
||||||
|
const { request } = await createAuthTestApp({
|
||||||
|
permission: ["system.access.api", "system.info"],
|
||||||
|
request: new Request("http://localhost/api/system/info"),
|
||||||
|
});
|
||||||
|
expect((await request.guest()).status).toBe(403);
|
||||||
|
expect((await request.member()).status).toBe(403);
|
||||||
|
expect((await request.authorized()).status).toBe(200);
|
||||||
|
expect((await request.admin()).status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reading permissions", async () => {
|
||||||
|
const { request } = await createAuthTestApp({
|
||||||
|
permission: ["system.access.api", "system.schema.read"],
|
||||||
|
request: new Request("http://localhost/api/system/permissions"),
|
||||||
|
});
|
||||||
|
expect((await request.guest()).status).toBe(403);
|
||||||
|
expect((await request.member()).status).toBe(403);
|
||||||
|
expect((await request.authorized()).status).toBe(200);
|
||||||
|
expect((await request.admin()).status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("access openapi", async () => {
|
||||||
|
const { request } = await createAuthTestApp({
|
||||||
|
permission: ["system.access.api", "system.openapi"],
|
||||||
|
request: new Request("http://localhost/api/system/openapi.json"),
|
||||||
|
});
|
||||||
|
expect((await request.guest()).status).toBe(403);
|
||||||
|
expect((await request.member()).status).toBe(403);
|
||||||
|
expect((await request.authorized()).status).toBe(200);
|
||||||
|
expect((await request.admin()).status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
171
app/__test__/auth/authorize/http/shared.ts
Normal file
171
app/__test__/auth/authorize/http/shared.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { createApp } from "core/test/utils";
|
||||||
|
import type { CreateAppConfig } from "App";
|
||||||
|
import type { RoleSchema } from "auth/authorize/Role";
|
||||||
|
import { isPlainObject } from "core/utils";
|
||||||
|
|
||||||
|
export type AuthTestConfig = {
|
||||||
|
guest?: RoleSchema;
|
||||||
|
member?: RoleSchema;
|
||||||
|
authorized?: RoleSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createAuthTestApp(
|
||||||
|
testConfig: {
|
||||||
|
permission: AuthTestConfig | string | string[];
|
||||||
|
request: Request;
|
||||||
|
},
|
||||||
|
config: Partial<CreateAppConfig> = {},
|
||||||
|
) {
|
||||||
|
let member: RoleSchema | undefined;
|
||||||
|
let authorized: RoleSchema | undefined;
|
||||||
|
let guest: RoleSchema | undefined;
|
||||||
|
if (isPlainObject(testConfig.permission)) {
|
||||||
|
if (testConfig.permission.guest)
|
||||||
|
guest = {
|
||||||
|
...testConfig.permission.guest,
|
||||||
|
is_default: true,
|
||||||
|
};
|
||||||
|
if (testConfig.permission.member) member = testConfig.permission.member;
|
||||||
|
if (testConfig.permission.authorized) authorized = testConfig.permission.authorized;
|
||||||
|
} else {
|
||||||
|
member = {
|
||||||
|
permissions: [],
|
||||||
|
};
|
||||||
|
authorized = {
|
||||||
|
permissions: Array.isArray(testConfig.permission)
|
||||||
|
? testConfig.permission
|
||||||
|
: [testConfig.permission],
|
||||||
|
};
|
||||||
|
guest = {
|
||||||
|
permissions: [],
|
||||||
|
is_default: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("authorized", authorized);
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
...config,
|
||||||
|
config: {
|
||||||
|
...config.config,
|
||||||
|
auth: {
|
||||||
|
...config.config?.auth,
|
||||||
|
enabled: true,
|
||||||
|
guard: {
|
||||||
|
enabled: true,
|
||||||
|
...config.config?.auth?.guard,
|
||||||
|
},
|
||||||
|
jwt: {
|
||||||
|
...config.config?.auth?.jwt,
|
||||||
|
secret: "secret",
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
...config.config?.auth?.roles,
|
||||||
|
guest,
|
||||||
|
member,
|
||||||
|
authorized,
|
||||||
|
admin: {
|
||||||
|
implicit_allow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
const users = {
|
||||||
|
guest: null,
|
||||||
|
member: await app.createUser({
|
||||||
|
email: "member@test.com",
|
||||||
|
password: "12345678",
|
||||||
|
role: "member",
|
||||||
|
}),
|
||||||
|
authorized: await app.createUser({
|
||||||
|
email: "authorized@test.com",
|
||||||
|
password: "12345678",
|
||||||
|
role: "authorized",
|
||||||
|
}),
|
||||||
|
admin: await app.createUser({
|
||||||
|
email: "admin@test.com",
|
||||||
|
password: "12345678",
|
||||||
|
role: "admin",
|
||||||
|
}),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const tokens = {} as Record<keyof typeof users, string>;
|
||||||
|
for (const [key, user] of Object.entries(users)) {
|
||||||
|
if (user) {
|
||||||
|
tokens[key as keyof typeof users] = await app.module.auth.authenticator.jwt(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeRequest(user: keyof typeof users, input: string, init: RequestInit = {}) {
|
||||||
|
const headers = new Headers(init.headers ?? {});
|
||||||
|
if (user in tokens) {
|
||||||
|
headers.set("Authorization", `Bearer ${tokens[user as keyof typeof tokens]}`);
|
||||||
|
}
|
||||||
|
const res = await app.server.request(input, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: any;
|
||||||
|
if (res.headers.get("Content-Type")?.startsWith("application/json")) {
|
||||||
|
data = await res.json();
|
||||||
|
} else if (res.headers.get("Content-Type")?.startsWith("text/")) {
|
||||||
|
data = await res.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
ok: res.ok,
|
||||||
|
headers: Object.fromEntries(res.headers.entries()),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestFn = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(_, prop: keyof typeof users) {
|
||||||
|
return async (input: string, init: RequestInit = {}) => {
|
||||||
|
return makeRequest(prop, input, init);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
) as {
|
||||||
|
[K in keyof typeof users]: (
|
||||||
|
input: string,
|
||||||
|
init?: RequestInit,
|
||||||
|
) => Promise<{
|
||||||
|
status: number;
|
||||||
|
ok: boolean;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
data: any;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(_, prop: keyof typeof users) {
|
||||||
|
return async () => {
|
||||||
|
return makeRequest(prop, testConfig.request.url, {
|
||||||
|
headers: testConfig.request.headers,
|
||||||
|
method: testConfig.request.method,
|
||||||
|
body: testConfig.request.body,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
) as {
|
||||||
|
[K in keyof typeof users]: () => Promise<{
|
||||||
|
status: number;
|
||||||
|
ok: boolean;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
data: any;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { app, users, request, requestFn };
|
||||||
|
}
|
||||||
85
app/__test__/data/postgres.test.ts
Normal file
85
app/__test__/data/postgres.test.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, beforeAll, afterAll, test } from "bun:test";
|
||||||
|
import type { PostgresConnection } from "data/connection/postgres";
|
||||||
|
import { pg, postgresJs } from "bknd";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import postgres from 'postgres'
|
||||||
|
import { disableConsoleLog, enableConsoleLog, $waitUntil } from "bknd/utils";
|
||||||
|
import { $ } from "bun";
|
||||||
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
|
const credentials = {
|
||||||
|
host: "localhost",
|
||||||
|
port: 5430,
|
||||||
|
user: "postgres",
|
||||||
|
password: "postgres",
|
||||||
|
database: "bknd",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function cleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
|
||||||
|
const kysely = connection.kysely;
|
||||||
|
|
||||||
|
// drop all tables+indexes & create new schema
|
||||||
|
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
|
||||||
|
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
|
||||||
|
await kysely.schema.createSchema("public").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isPostgresRunning() {
|
||||||
|
try {
|
||||||
|
// Try to actually connect to PostgreSQL
|
||||||
|
const conn = pg({ pool: new Pool(credentials) });
|
||||||
|
await conn.ping();
|
||||||
|
await conn.close();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("postgres", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
if (!(await isPostgresRunning())) {
|
||||||
|
await $`docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=${credentials.password} -e POSTGRES_USER=${credentials.user} -e POSTGRES_DB=${credentials.database} -p ${credentials.port}:5432 postgres:17`;
|
||||||
|
await $waitUntil("Postgres is running", isPostgresRunning);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
disableConsoleLog();
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
if (await isPostgresRunning()) {
|
||||||
|
try {
|
||||||
|
await $`docker stop bknd-test-postgres`;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enableConsoleLog();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.serial.each([
|
||||||
|
["pg", () => pg({ pool: new Pool(credentials) })],
|
||||||
|
["postgresjs", () => postgresJs({ postgres: postgres(credentials) })],
|
||||||
|
])("%s", (name, createConnection) => {
|
||||||
|
connectionTestSuite(
|
||||||
|
{
|
||||||
|
...bunTestRunner,
|
||||||
|
test: test.serial,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
makeConnection: () => {
|
||||||
|
const connection = createConnection();
|
||||||
|
return {
|
||||||
|
connection,
|
||||||
|
dispose: async () => {
|
||||||
|
await cleanDatabase(connection);
|
||||||
|
await connection.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rawDialectDetails: [],
|
||||||
|
disableConsoleLog: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -124,6 +124,81 @@ describe("[Repository]", async () => {
|
|||||||
.then((r) => [r.count, r.total]),
|
.then((r) => [r.count, r.total]),
|
||||||
).resolves.toEqual([undefined, undefined]);
|
).resolves.toEqual([undefined, undefined]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("auto join", async () => {
|
||||||
|
const schema = $em(
|
||||||
|
{
|
||||||
|
posts: $entity("posts", {
|
||||||
|
title: $text(),
|
||||||
|
content: $text(),
|
||||||
|
}),
|
||||||
|
comments: $entity("comments", {
|
||||||
|
content: $text(),
|
||||||
|
}),
|
||||||
|
another: $entity("another", {
|
||||||
|
title: $text(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
({ relation }, { posts, comments }) => {
|
||||||
|
relation(comments).manyToOne(posts);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const em = schema.proto.withConnection(getDummyConnection().dummyConnection);
|
||||||
|
await em.schema().sync({ force: true });
|
||||||
|
|
||||||
|
await em.mutator("posts").insertOne({ title: "post1", content: "content1" });
|
||||||
|
await em
|
||||||
|
.mutator("comments")
|
||||||
|
.insertMany([{ content: "comment1", posts_id: 1 }, { content: "comment2" }] as any);
|
||||||
|
|
||||||
|
const res = await em.repo("comments").findMany({
|
||||||
|
where: {
|
||||||
|
"posts.title": "post1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.data as any).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: "comment1",
|
||||||
|
posts_id: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
{
|
||||||
|
// manual join should still work
|
||||||
|
const res = await em.repo("comments").findMany({
|
||||||
|
join: ["posts"],
|
||||||
|
where: {
|
||||||
|
"posts.title": "post1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.data as any).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: "comment1",
|
||||||
|
posts_id: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// inexistent should be detected and thrown
|
||||||
|
expect(
|
||||||
|
em.repo("comments").findMany({
|
||||||
|
where: {
|
||||||
|
"random.title": "post1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/Invalid where field/);
|
||||||
|
|
||||||
|
// existing alias, but not a relation should throw
|
||||||
|
expect(
|
||||||
|
em.repo("comments").findMany({
|
||||||
|
where: {
|
||||||
|
"another.title": "post1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/Invalid where field/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("[data] Repository (Events)", async () => {
|
describe("[data] Repository (Events)", async () => {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ describe("SqliteIntrospector", () => {
|
|||||||
dataType: "INTEGER",
|
dataType: "INTEGER",
|
||||||
isNullable: false,
|
isNullable: false,
|
||||||
isAutoIncrementing: true,
|
isAutoIncrementing: true,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: true,
|
||||||
comment: undefined,
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -89,7 +89,7 @@ describe("SqliteIntrospector", () => {
|
|||||||
dataType: "INTEGER",
|
dataType: "INTEGER",
|
||||||
isNullable: false,
|
isNullable: false,
|
||||||
isAutoIncrementing: true,
|
isAutoIncrementing: true,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: true,
|
||||||
comment: undefined,
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { assetsPath, assetsTmpPath } from "../helper";
|
|||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
//disableConsoleLog();
|
disableConsoleLog();
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
registries.media.register("local", StorageLocalAdapter);
|
||||||
});
|
});
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ beforeAll(disableConsoleLog);
|
|||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("AppAuth", () => {
|
describe("AppAuth", () => {
|
||||||
test.skip("...", () => {
|
|
||||||
const auth = new AppAuth({});
|
|
||||||
console.log(auth.toJSON());
|
|
||||||
console.log(auth.config);
|
|
||||||
});
|
|
||||||
|
|
||||||
moduleTestSuite(AppAuth);
|
moduleTestSuite(AppAuth);
|
||||||
|
|
||||||
let ctx: ModuleBuildContext;
|
let ctx: ModuleBuildContext;
|
||||||
@@ -39,11 +33,9 @@ describe("AppAuth", () => {
|
|||||||
await auth.build();
|
await auth.build();
|
||||||
|
|
||||||
const oldConfig = auth.toJSON(true);
|
const oldConfig = auth.toJSON(true);
|
||||||
//console.log(oldConfig);
|
|
||||||
await auth.schema().patch("enabled", true);
|
await auth.schema().patch("enabled", true);
|
||||||
await auth.build();
|
await auth.build();
|
||||||
const newConfig = auth.toJSON(true);
|
const newConfig = auth.toJSON(true);
|
||||||
//console.log(newConfig);
|
|
||||||
expect(newConfig.jwt.secret).not.toBe(oldConfig.jwt.secret);
|
expect(newConfig.jwt.secret).not.toBe(oldConfig.jwt.secret);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,7 +61,6 @@ describe("AppAuth", () => {
|
|||||||
const app = new AuthController(auth).getController();
|
const app = new AuthController(auth).getController();
|
||||||
|
|
||||||
{
|
{
|
||||||
disableConsoleLog();
|
|
||||||
const res = await app.request("/password/register", {
|
const res = await app.request("/password/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -80,7 +71,6 @@ describe("AppAuth", () => {
|
|||||||
password: "12345678",
|
password: "12345678",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
enableConsoleLog();
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
const { data: users } = await ctx.em.repository("users").findMany();
|
const { data: users } = await ctx.em.repository("users").findMany();
|
||||||
@@ -119,7 +109,6 @@ describe("AppAuth", () => {
|
|||||||
const app = new AuthController(auth).getController();
|
const app = new AuthController(auth).getController();
|
||||||
|
|
||||||
{
|
{
|
||||||
disableConsoleLog();
|
|
||||||
const res = await app.request("/password/register", {
|
const res = await app.request("/password/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -130,7 +119,6 @@ describe("AppAuth", () => {
|
|||||||
password: "12345678",
|
password: "12345678",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
enableConsoleLog();
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
const { data: users } = await ctx.em.repository("users").findMany();
|
const { data: users } = await ctx.em.repository("users").findMany();
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import { em, entity, text } from "data/prototype";
|
import { em, entity, text } from "data/prototype";
|
||||||
import { registries } from "modules/registries";
|
import { registries } from "modules/registries";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { AppMedia } from "../../src/media/AppMedia";
|
import { AppMedia } from "../../src/media/AppMedia";
|
||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("AppMedia", () => {
|
describe("AppMedia", () => {
|
||||||
test.skip("...", () => {
|
test.skip("...", () => {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { it, expect, describe } from "bun:test";
|
import { it, expect, describe, beforeAll, afterAll } from "bun:test";
|
||||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
import { DbModuleManager } from "modules/db/DbModuleManager";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { TABLE_NAME } from "modules/db/migrations";
|
import { TABLE_NAME } from "modules/db/migrations";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("DbModuleManager", () => {
|
describe("DbModuleManager", () => {
|
||||||
it("should extract secrets", async () => {
|
it("should extract secrets", async () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { s, stripMark } from "core/utils/schema";
|
|||||||
import { Connection } from "data/connection/Connection";
|
import { Connection } from "data/connection/Connection";
|
||||||
import { entity, text } from "data/prototype";
|
import { entity, text } from "data/prototype";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(() => disableConsoleLog());
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("ModuleManager", async () => {
|
describe("ModuleManager", async () => {
|
||||||
@@ -82,7 +82,6 @@ describe("ModuleManager", async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
//const { version, ...json } = mm.toJSON() as any;
|
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
const db = dummyConnection.kysely;
|
const db = dummyConnection.kysely;
|
||||||
@@ -97,10 +96,6 @@ describe("ModuleManager", async () => {
|
|||||||
|
|
||||||
await mm2.build();
|
await mm2.build();
|
||||||
|
|
||||||
/* console.log({
|
|
||||||
json,
|
|
||||||
configs: mm2.configs(),
|
|
||||||
}); */
|
|
||||||
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||||
expect(mm2.configs().data.entities?.test).toBeDefined();
|
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||||
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||||
@@ -228,8 +223,6 @@ describe("ModuleManager", async () => {
|
|||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
const mm = new ModuleManager(c.dummyConnection);
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
console.log("==".repeat(30));
|
|
||||||
console.log("");
|
|
||||||
const json = mm.configs();
|
const json = mm.configs();
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
@@ -275,7 +268,6 @@ describe("ModuleManager", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async build() {
|
override async build() {
|
||||||
//console.log("building FailingModule", this.config);
|
|
||||||
if (this.config.value && this.config.value < 0) {
|
if (this.config.value && this.config.value < 0) {
|
||||||
throw new Error("value must be positive, given: " + this.config.value);
|
throw new Error("value must be positive, given: " + this.config.value);
|
||||||
}
|
}
|
||||||
@@ -296,9 +288,6 @@ describe("ModuleManager", async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => disableConsoleLog(["log", "warn", "error"]));
|
|
||||||
afterEach(enableConsoleLog);
|
|
||||||
|
|
||||||
test("it builds", async () => {
|
test("it builds", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
const mm = new TestModuleManager(dummyConnection);
|
const mm = new TestModuleManager(dummyConnection);
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ async function buildUiElements() {
|
|||||||
outDir: "dist/ui/elements",
|
outDir: "dist/ui/elements",
|
||||||
external: [
|
external: [
|
||||||
"ui/client",
|
"ui/client",
|
||||||
|
"bknd",
|
||||||
|
/^bknd\/.*/,
|
||||||
"react",
|
"react",
|
||||||
"react-dom",
|
"react-dom",
|
||||||
"react/jsx-runtime",
|
"react/jsx-runtime",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/bknd-io/bknd/issues"
|
"url": "https://github.com/bknd-io/bknd/issues"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.3.2",
|
"packageManager": "bun@1.3.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.13"
|
"node": ">=22.13"
|
||||||
},
|
},
|
||||||
@@ -99,6 +99,7 @@
|
|||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
|
"@types/pg": "^8.15.6",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
@@ -110,14 +111,17 @@
|
|||||||
"jotai": "^2.12.2",
|
"jotai": "^2.12.2",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"kysely-generic-sqlite": "^1.2.1",
|
"kysely-generic-sqlite": "^1.2.1",
|
||||||
|
"kysely-postgres-js": "^2.0.0",
|
||||||
"libsql": "^0.5.22",
|
"libsql": "^0.5.22",
|
||||||
"libsql-stateless-easy": "^1.8.0",
|
"libsql-stateless-easy": "^1.8.0",
|
||||||
"miniflare": "^4.20251011.2",
|
"miniflare": "^4.20251011.2",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
"posthog-js-lite": "^3.6.0",
|
"posthog-js-lite": "^3.6.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { MaybePromise } from "bknd";
|
||||||
import type { Event } from "./Event";
|
import type { Event } from "./Event";
|
||||||
import type { EventClass } from "./EventManager";
|
import type { EventClass } from "./EventManager";
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ export type ListenerMode = (typeof ListenerModes)[number];
|
|||||||
export type ListenerHandler<E extends Event<any, any>> = (
|
export type ListenerHandler<E extends Event<any, any>> = (
|
||||||
event: E,
|
event: E,
|
||||||
slug: string,
|
slug: string,
|
||||||
) => E extends Event<any, infer R> ? R | Promise<R | void> : never;
|
) => E extends Event<any, infer R> ? MaybePromise<R | void> : never;
|
||||||
|
|
||||||
export class EventListener<E extends Event = Event> {
|
export class EventListener<E extends Event = Event> {
|
||||||
mode: ListenerMode = "async";
|
mode: ListenerMode = "async";
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export function isObject(value: unknown): value is Record<string, unknown> {
|
|||||||
|
|
||||||
export function omitKeys<T extends object, K extends keyof T>(
|
export function omitKeys<T extends object, K extends keyof T>(
|
||||||
obj: T,
|
obj: T,
|
||||||
keys_: readonly K[],
|
keys_: readonly K[] | K[] | string[],
|
||||||
): Omit<T, Extract<K, keyof T>> {
|
): Omit<T, Extract<K, keyof T>> {
|
||||||
const keys = new Set(keys_);
|
const keys = new Set(keys_ as readonly K[]);
|
||||||
const result = {} as Omit<T, Extract<K, keyof T>>;
|
const result = {} as Omit<T, Extract<K, keyof T>>;
|
||||||
for (const [key, value] of Object.entries(obj) as [keyof T, T[keyof T]][]) {
|
for (const [key, value] of Object.entries(obj) as [keyof T, T[keyof T]][]) {
|
||||||
if (!keys.has(key as K)) {
|
if (!keys.has(key as K)) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { MaybePromise } from "core/types";
|
||||||
import { getRuntimeKey as honoGetRuntimeKey } from "hono/adapter";
|
import { getRuntimeKey as honoGetRuntimeKey } from "hono/adapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,3 +78,37 @@ export function threw(fn: () => any, instance?: new (...args: any[]) => Error) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function threwAsync(fn: Promise<any>, instance?: new (...args: any[]) => Error) {
|
||||||
|
try {
|
||||||
|
await fn;
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
if (instance) {
|
||||||
|
if (e instanceof instance) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// if instance given but not what expected, throw
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function $waitUntil(
|
||||||
|
message: string,
|
||||||
|
condition: () => MaybePromise<boolean>,
|
||||||
|
delay = 100,
|
||||||
|
maxAttempts = 10,
|
||||||
|
) {
|
||||||
|
let attempts = 0;
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
if (await condition()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`$waitUntil: "${message}" failed after ${maxAttempts} attempts`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ export class DataController extends Controller {
|
|||||||
// read entity schema
|
// read entity schema
|
||||||
hono.get(
|
hono.get(
|
||||||
"/schema.json",
|
"/schema.json",
|
||||||
|
permission(SystemPermissions.schemaRead, {
|
||||||
|
context: (_c) => ({ module: "data" }),
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityRead, {
|
permission(DataPermissions.entityRead, {
|
||||||
context: (c) => ({ entity: c.req.param("entity") }),
|
context: (c) => ({ entity: c.req.param("entity") }),
|
||||||
}),
|
}),
|
||||||
@@ -124,6 +127,9 @@ export class DataController extends Controller {
|
|||||||
// read schema
|
// read schema
|
||||||
hono.get(
|
hono.get(
|
||||||
"/schemas/:entity/:context?",
|
"/schemas/:entity/:context?",
|
||||||
|
permission(SystemPermissions.schemaRead, {
|
||||||
|
context: (_c) => ({ module: "data" }),
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityRead, {
|
permission(DataPermissions.entityRead, {
|
||||||
context: (c) => ({ entity: c.req.param("entity") }),
|
context: (c) => ({ entity: c.req.param("entity") }),
|
||||||
}),
|
}),
|
||||||
@@ -161,7 +167,7 @@ export class DataController extends Controller {
|
|||||||
hono.get(
|
hono.get(
|
||||||
"/types",
|
"/types",
|
||||||
permission(SystemPermissions.schemaRead, {
|
permission(SystemPermissions.schemaRead, {
|
||||||
context: (c) => ({ module: "data" }),
|
context: (_c) => ({ module: "data" }),
|
||||||
}),
|
}),
|
||||||
describeRoute({
|
describeRoute({
|
||||||
summary: "Retrieve data typescript definitions",
|
summary: "Retrieve data typescript definitions",
|
||||||
@@ -182,6 +188,9 @@ export class DataController extends Controller {
|
|||||||
*/
|
*/
|
||||||
hono.get(
|
hono.get(
|
||||||
"/info/:entity",
|
"/info/:entity",
|
||||||
|
permission(SystemPermissions.schemaRead, {
|
||||||
|
context: (_c) => ({ module: "data" }),
|
||||||
|
}),
|
||||||
permission(DataPermissions.entityRead, {
|
permission(DataPermissions.entityRead, {
|
||||||
context: (c) => ({ entity: c.req.param("entity") }),
|
context: (c) => ({ entity: c.req.param("entity") }),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -14,27 +14,31 @@ export function connectionTestSuite(
|
|||||||
{
|
{
|
||||||
makeConnection,
|
makeConnection,
|
||||||
rawDialectDetails,
|
rawDialectDetails,
|
||||||
|
disableConsoleLog: _disableConsoleLog = true,
|
||||||
}: {
|
}: {
|
||||||
makeConnection: () => MaybePromise<{
|
makeConnection: () => MaybePromise<{
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
dispose: () => MaybePromise<void>;
|
dispose: () => MaybePromise<void>;
|
||||||
}>;
|
}>;
|
||||||
rawDialectDetails: string[];
|
rawDialectDetails: string[];
|
||||||
|
disableConsoleLog?: boolean;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { test, expect, describe, beforeEach, afterEach, afterAll, beforeAll } = testRunner;
|
const { test, expect, describe, beforeEach, afterEach, afterAll, beforeAll } = testRunner;
|
||||||
beforeAll(() => disableConsoleLog());
|
if (_disableConsoleLog) {
|
||||||
afterAll(() => enableConsoleLog());
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(() => enableConsoleLog());
|
||||||
|
}
|
||||||
|
|
||||||
describe("base", () => {
|
let ctx: Awaited<ReturnType<typeof makeConnection>>;
|
||||||
let ctx: Awaited<ReturnType<typeof makeConnection>>;
|
beforeEach(async () => {
|
||||||
beforeEach(async () => {
|
ctx = await makeConnection();
|
||||||
ctx = await makeConnection();
|
});
|
||||||
});
|
afterEach(async () => {
|
||||||
afterEach(async () => {
|
await ctx.dispose();
|
||||||
await ctx.dispose();
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
describe("base", async () => {
|
||||||
test("pings", async () => {
|
test("pings", async () => {
|
||||||
const res = await ctx.connection.ping();
|
const res = await ctx.connection.ping();
|
||||||
expect(res).toBe(true);
|
expect(res).toBe(true);
|
||||||
@@ -98,52 +102,54 @@ export function connectionTestSuite(
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("schema", async () => {
|
describe("schema", async () => {
|
||||||
const { connection, dispose } = await makeConnection();
|
const makeSchema = async () => {
|
||||||
afterAll(async () => {
|
const fields = [
|
||||||
await dispose();
|
{
|
||||||
});
|
type: "integer",
|
||||||
|
name: "id",
|
||||||
|
primary: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "json",
|
||||||
|
name: "json",
|
||||||
|
},
|
||||||
|
] as const satisfies FieldSpec[];
|
||||||
|
|
||||||
const fields = [
|
let b = ctx.connection.kysely.schema.createTable("test");
|
||||||
{
|
for (const field of fields) {
|
||||||
type: "integer",
|
// @ts-expect-error
|
||||||
name: "id",
|
b = b.addColumn(...ctx.connection.getFieldSchema(field));
|
||||||
primary: true,
|
}
|
||||||
},
|
await b.execute();
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
name: "text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "json",
|
|
||||||
name: "json",
|
|
||||||
},
|
|
||||||
] as const satisfies FieldSpec[];
|
|
||||||
|
|
||||||
let b = connection.kysely.schema.createTable("test");
|
// add index
|
||||||
for (const field of fields) {
|
await ctx.connection.kysely.schema
|
||||||
// @ts-expect-error
|
.createIndex("test_index")
|
||||||
b = b.addColumn(...connection.getFieldSchema(field));
|
.on("test")
|
||||||
}
|
.columns(["id"])
|
||||||
await b.execute();
|
.execute();
|
||||||
|
};
|
||||||
// add index
|
|
||||||
await connection.kysely.schema.createIndex("test_index").on("test").columns(["id"]).execute();
|
|
||||||
|
|
||||||
test("executes query", async () => {
|
test("executes query", async () => {
|
||||||
await connection.kysely
|
await makeSchema();
|
||||||
|
await ctx.connection.kysely
|
||||||
.insertInto("test")
|
.insertInto("test")
|
||||||
.values({ id: 1, text: "test", json: JSON.stringify({ a: 1 }) })
|
.values({ id: 1, text: "test", json: JSON.stringify({ a: 1 }) })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const expected = { id: 1, text: "test", json: { a: 1 } };
|
const expected = { id: 1, text: "test", json: { a: 1 } };
|
||||||
|
|
||||||
const qb = connection.kysely.selectFrom("test").selectAll();
|
const qb = ctx.connection.kysely.selectFrom("test").selectAll();
|
||||||
const res = await connection.executeQuery(qb);
|
const res = await ctx.connection.executeQuery(qb);
|
||||||
expect(res.rows).toEqual([expected]);
|
expect(res.rows).toEqual([expected]);
|
||||||
expect(rawDialectDetails.every((detail) => getPath(res, detail) !== undefined)).toBe(true);
|
expect(rawDialectDetails.every((detail) => getPath(res, detail) !== undefined)).toBe(true);
|
||||||
|
|
||||||
{
|
{
|
||||||
const res = await connection.executeQueries(qb, qb);
|
const res = await ctx.connection.executeQueries(qb, qb);
|
||||||
expect(res.length).toBe(2);
|
expect(res.length).toBe(2);
|
||||||
res.map((r) => {
|
res.map((r) => {
|
||||||
expect(r.rows).toEqual([expected]);
|
expect(r.rows).toEqual([expected]);
|
||||||
@@ -155,15 +161,21 @@ export function connectionTestSuite(
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("introspects", async () => {
|
test("introspects", async () => {
|
||||||
const tables = await connection.getIntrospector().getTables({
|
await makeSchema();
|
||||||
|
const tables = await ctx.connection.getIntrospector().getTables({
|
||||||
withInternalKyselyTables: false,
|
withInternalKyselyTables: false,
|
||||||
});
|
});
|
||||||
const clean = tables.map((t) => ({
|
const clean = tables.map((t) => ({
|
||||||
...t,
|
...t,
|
||||||
columns: t.columns.map((c) => ({
|
columns: t.columns
|
||||||
...c,
|
.map((c) => ({
|
||||||
dataType: undefined,
|
...c,
|
||||||
})),
|
// ignore data type
|
||||||
|
dataType: undefined,
|
||||||
|
// ignore default value if "id"
|
||||||
|
hasDefaultValue: c.name !== "id" ? c.hasDefaultValue : undefined,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
expect(clean).toEqual([
|
expect(clean).toEqual([
|
||||||
@@ -176,14 +188,8 @@ export function connectionTestSuite(
|
|||||||
dataType: undefined,
|
dataType: undefined,
|
||||||
isNullable: false,
|
isNullable: false,
|
||||||
isAutoIncrementing: true,
|
isAutoIncrementing: true,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: undefined,
|
||||||
},
|
comment: undefined,
|
||||||
{
|
|
||||||
name: "text",
|
|
||||||
dataType: undefined,
|
|
||||||
isNullable: true,
|
|
||||||
isAutoIncrementing: false,
|
|
||||||
hasDefaultValue: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "json",
|
name: "json",
|
||||||
@@ -191,25 +197,34 @@ export function connectionTestSuite(
|
|||||||
isNullable: true,
|
isNullable: true,
|
||||||
isAutoIncrementing: false,
|
isAutoIncrementing: false,
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
|
comment: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
dataType: undefined,
|
||||||
|
isNullable: true,
|
||||||
|
isAutoIncrementing: false,
|
||||||
|
hasDefaultValue: false,
|
||||||
|
comment: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(await ctx.connection.getIntrospector().getIndices()).toEqual([
|
||||||
|
{
|
||||||
|
name: "test_index",
|
||||||
|
table: "test",
|
||||||
|
isUnique: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
order: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await connection.getIntrospector().getIndices()).toEqual([
|
|
||||||
{
|
|
||||||
name: "test_index",
|
|
||||||
table: "test",
|
|
||||||
isUnique: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: "id",
|
|
||||||
order: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("integration", async () => {
|
describe("integration", async () => {
|
||||||
|
|||||||
33
app/src/data/connection/postgres/PgPostgresConnection.ts
Normal file
33
app/src/data/connection/postgres/PgPostgresConnection.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Kysely, PostgresDialect, type PostgresDialectConfig as KyselyPostgresDialectConfig } from "kysely";
|
||||||
|
import { PostgresIntrospector } from "./PostgresIntrospector";
|
||||||
|
import { PostgresConnection, plugins } from "./PostgresConnection";
|
||||||
|
import { customIntrospector } from "../Connection";
|
||||||
|
import type { Pool } from "pg";
|
||||||
|
|
||||||
|
export type PostgresDialectConfig = Omit<KyselyPostgresDialectConfig, "pool"> & {
|
||||||
|
pool: Pool;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PgPostgresConnection extends PostgresConnection<Pool> {
|
||||||
|
override name = "pg";
|
||||||
|
|
||||||
|
constructor(config: PostgresDialectConfig) {
|
||||||
|
const kysely = new Kysely({
|
||||||
|
dialect: customIntrospector(PostgresDialect, PostgresIntrospector, {
|
||||||
|
excludeTables: [],
|
||||||
|
}).create(config),
|
||||||
|
plugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
super(kysely);
|
||||||
|
this.client = config.pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async close(): Promise<void> {
|
||||||
|
await this.client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pg(config: PostgresDialectConfig): PgPostgresConnection {
|
||||||
|
return new PgPostgresConnection(config);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
type SchemaResponse,
|
type SchemaResponse,
|
||||||
type ConnQuery,
|
type ConnQuery,
|
||||||
type ConnQueryResults,
|
type ConnQueryResults,
|
||||||
} from "bknd";
|
} from "../Connection";
|
||||||
import {
|
import {
|
||||||
ParseJSONResultsPlugin,
|
ParseJSONResultsPlugin,
|
||||||
type ColumnDataType,
|
type ColumnDataType,
|
||||||
@@ -20,7 +20,7 @@ export type QB = SelectQueryBuilder<any, any, any>;
|
|||||||
|
|
||||||
export const plugins = [new ParseJSONResultsPlugin()];
|
export const plugins = [new ParseJSONResultsPlugin()];
|
||||||
|
|
||||||
export abstract class PostgresConnection extends Connection {
|
export abstract class PostgresConnection<Client = unknown> extends Connection<Client> {
|
||||||
protected override readonly supported = {
|
protected override readonly supported = {
|
||||||
batching: true,
|
batching: true,
|
||||||
softscans: true,
|
softscans: true,
|
||||||
@@ -68,7 +68,7 @@ export abstract class PostgresConnection extends Connection {
|
|||||||
type,
|
type,
|
||||||
(col: ColumnDefinitionBuilder) => {
|
(col: ColumnDefinitionBuilder) => {
|
||||||
if (spec.primary) {
|
if (spec.primary) {
|
||||||
return col.primaryKey();
|
return col.primaryKey().notNull();
|
||||||
}
|
}
|
||||||
if (spec.references) {
|
if (spec.references) {
|
||||||
return col
|
return col
|
||||||
@@ -76,7 +76,7 @@ export abstract class PostgresConnection extends Connection {
|
|||||||
.onDelete(spec.onDelete ?? "set null")
|
.onDelete(spec.onDelete ?? "set null")
|
||||||
.onUpdate(spec.onUpdate ?? "no action");
|
.onUpdate(spec.onUpdate ?? "no action");
|
||||||
}
|
}
|
||||||
return spec.nullable ? col : col.notNull();
|
return col;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type SchemaMetadata, sql } from "kysely";
|
import { type SchemaMetadata, sql } from "kysely";
|
||||||
import { BaseIntrospector } from "bknd";
|
import { BaseIntrospector } from "../BaseIntrospector";
|
||||||
|
|
||||||
type PostgresSchemaSpec = {
|
type PostgresSchemaSpec = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -102,26 +102,27 @@ export class PostgresIntrospector extends BaseIntrospector {
|
|||||||
return tables.map((table) => ({
|
return tables.map((table) => ({
|
||||||
name: table.name,
|
name: table.name,
|
||||||
isView: table.type === "VIEW",
|
isView: table.type === "VIEW",
|
||||||
columns: table.columns.map((col) => {
|
columns: table.columns.map((col) => ({
|
||||||
return {
|
name: col.name,
|
||||||
name: col.name,
|
dataType: col.type,
|
||||||
dataType: col.type,
|
isNullable: !col.notnull,
|
||||||
isNullable: !col.notnull,
|
isAutoIncrementing: col.dflt?.toLowerCase().includes("nextval") ?? false,
|
||||||
// @todo: check default value on 'nextval' see https://www.postgresql.org/docs/17/datatype-numeric.html#DATATYPE-SERIAL
|
hasDefaultValue: col.dflt != null,
|
||||||
isAutoIncrementing: true, // just for now
|
comment: undefined,
|
||||||
hasDefaultValue: col.dflt != null,
|
|
||||||
comment: undefined,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
indices: table.indices.map((index) => ({
|
|
||||||
name: index.name,
|
|
||||||
table: table.name,
|
|
||||||
isUnique: index.sql?.match(/unique/i) != null,
|
|
||||||
columns: index.columns.map((col) => ({
|
|
||||||
name: col.name,
|
|
||||||
order: col.seqno,
|
|
||||||
})),
|
|
||||||
})),
|
})),
|
||||||
|
indices: table.indices
|
||||||
|
// filter out db-managed primary key index
|
||||||
|
.filter((index) => index.name !== `${table.name}_pkey`)
|
||||||
|
.map((index) => ({
|
||||||
|
name: index.name,
|
||||||
|
table: table.name,
|
||||||
|
isUnique: index.sql?.match(/unique/i) != null,
|
||||||
|
columns: index.columns.map((col) => ({
|
||||||
|
name: col.name,
|
||||||
|
// seqno starts at 1
|
||||||
|
order: col.seqno - 1,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
31
app/src/data/connection/postgres/PostgresJsConnection.ts
Normal file
31
app/src/data/connection/postgres/PostgresJsConnection.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Kysely } from "kysely";
|
||||||
|
import { PostgresIntrospector } from "./PostgresIntrospector";
|
||||||
|
import { PostgresConnection, plugins } from "./PostgresConnection";
|
||||||
|
import { customIntrospector } from "../Connection";
|
||||||
|
import { PostgresJSDialect, type PostgresJSDialectConfig } from "kysely-postgres-js";
|
||||||
|
|
||||||
|
export class PostgresJsConnection extends PostgresConnection<PostgresJSDialectConfig["postgres"]> {
|
||||||
|
override name = "postgres-js";
|
||||||
|
|
||||||
|
constructor(config: PostgresJSDialectConfig) {
|
||||||
|
const kysely = new Kysely({
|
||||||
|
dialect: customIntrospector(PostgresJSDialect, PostgresIntrospector, {
|
||||||
|
excludeTables: [],
|
||||||
|
}).create(config),
|
||||||
|
plugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
super(kysely);
|
||||||
|
this.client = config.postgres;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async close(): Promise<void> {
|
||||||
|
await this.client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postgresJs(
|
||||||
|
config: PostgresJSDialectConfig,
|
||||||
|
): PostgresJsConnection {
|
||||||
|
return new PostgresJsConnection(config);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { customIntrospector, type DbFunctions } from "bknd";
|
import { customIntrospector, type DbFunctions } from "../Connection";
|
||||||
import { Kysely, type Dialect, type KyselyPlugin } from "kysely";
|
import { Kysely, type Dialect, type KyselyPlugin } from "kysely";
|
||||||
import { plugins, PostgresConnection } from "./PostgresConnection";
|
import { plugins, PostgresConnection } from "./PostgresConnection";
|
||||||
import { PostgresIntrospector } from "./PostgresIntrospector";
|
import { PostgresIntrospector } from "./PostgresIntrospector";
|
||||||
@@ -6,7 +6,7 @@ import { PostgresIntrospector } from "./PostgresIntrospector";
|
|||||||
export type Constructor<T> = new (...args: any[]) => T;
|
export type Constructor<T> = new (...args: any[]) => T;
|
||||||
|
|
||||||
export type CustomPostgresConnection = {
|
export type CustomPostgresConnection = {
|
||||||
supports?: PostgresConnection["supported"];
|
supports?: Partial<PostgresConnection["supported"]>;
|
||||||
fn?: Partial<DbFunctions>;
|
fn?: Partial<DbFunctions>;
|
||||||
plugins?: KyselyPlugin[];
|
plugins?: KyselyPlugin[];
|
||||||
excludeTables?: string[];
|
excludeTables?: string[];
|
||||||
@@ -13,7 +13,6 @@ import { customIntrospector } from "../Connection";
|
|||||||
import { SqliteIntrospector } from "./SqliteIntrospector";
|
import { SqliteIntrospector } from "./SqliteIntrospector";
|
||||||
import type { Field } from "data/fields/Field";
|
import type { Field } from "data/fields/Field";
|
||||||
|
|
||||||
// @todo: add pragmas
|
|
||||||
export type SqliteConnectionConfig<
|
export type SqliteConnectionConfig<
|
||||||
CustomDialect extends Constructor<Dialect> = Constructor<Dialect>,
|
CustomDialect extends Constructor<Dialect> = Constructor<Dialect>,
|
||||||
> = {
|
> = {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export class SqliteIntrospector extends BaseIntrospector {
|
|||||||
dataType: col.type,
|
dataType: col.type,
|
||||||
isNullable: !col.notnull,
|
isNullable: !col.notnull,
|
||||||
isAutoIncrementing: col.name === autoIncrementCol,
|
isAutoIncrementing: col.name === autoIncrementCol,
|
||||||
hasDefaultValue: col.dflt_value != null,
|
hasDefaultValue: col.name === autoIncrementCol ? true : col.dflt_value != null,
|
||||||
comment: undefined,
|
comment: undefined,
|
||||||
};
|
};
|
||||||
}) ?? [],
|
}) ?? [],
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
validated.with = options.with;
|
validated.with = options.with;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add explicit joins. Implicit joins are added in `where` builder
|
||||||
if (options.join && options.join.length > 0) {
|
if (options.join && options.join.length > 0) {
|
||||||
for (const entry of options.join) {
|
for (const entry of options.join) {
|
||||||
const related = this.em.relationOf(entity.name, entry);
|
const related = this.em.relationOf(entity.name, entry);
|
||||||
@@ -127,12 +128,28 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
const invalid = WhereBuilder.getPropertyNames(options.where).filter((field) => {
|
const invalid = WhereBuilder.getPropertyNames(options.where).filter((field) => {
|
||||||
if (field.includes(".")) {
|
if (field.includes(".")) {
|
||||||
const [alias, prop] = field.split(".") as [string, string];
|
const [alias, prop] = field.split(".") as [string, string];
|
||||||
if (!aliases.includes(alias)) {
|
// check aliases first (added joins)
|
||||||
|
if (aliases.includes(alias)) {
|
||||||
|
this.checkIndex(alias, prop, "where");
|
||||||
|
return !this.em.entity(alias).getField(prop);
|
||||||
|
}
|
||||||
|
// check if alias (entity) exists
|
||||||
|
if (!this.em.hasEntity(alias)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// check related fields for auto join
|
||||||
|
const related = this.em.relationOf(entity.name, alias);
|
||||||
|
if (related) {
|
||||||
|
const other = related.other(entity);
|
||||||
|
if (other.entity.getField(prop)) {
|
||||||
|
// if related field is found, add join to validated options
|
||||||
|
validated.join?.push(alias);
|
||||||
|
this.checkIndex(alias, prop, "where");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.checkIndex(alias, prop, "where");
|
return true;
|
||||||
return !this.em.entity(alias).getField(prop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkIndex(entity.name, field, "where");
|
this.checkIndex(entity.name, field, "where");
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export class NumberField<Required extends true | false = false> extends Field<
|
|||||||
|
|
||||||
switch (context) {
|
switch (context) {
|
||||||
case "submit":
|
case "submit":
|
||||||
return Number.parseInt(value);
|
return Number.parseInt(value, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function getChangeSet(
|
|||||||
const value = _value === "" ? null : _value;
|
const value = _value === "" ? null : _value;
|
||||||
|
|
||||||
// normalize to null if undefined
|
// normalize to null if undefined
|
||||||
const newValue = field.getValue(value, "submit") || null;
|
const newValue = field.getValue(value, "submit") ?? null;
|
||||||
// @todo: add typing for "action"
|
// @todo: add typing for "action"
|
||||||
if (action === "create" || newValue !== data[key]) {
|
if (action === "create" || newValue !== data[key]) {
|
||||||
acc[key] = newValue;
|
acc[key] = newValue;
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ class EntityManagerPrototype<Entities extends Record<string, Entity>> extends En
|
|||||||
super(Object.values(__entities), new DummyConnection(), relations, indices);
|
super(Object.values(__entities), new DummyConnection(), relations, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
withConnection(connection: Connection): EntityManager<Schema<Entities>> {
|
withConnection(connection: Connection): EntityManager<Schemas<Entities>> {
|
||||||
return new EntityManager(this.entities, connection, this.relations.all, this.indices);
|
return new EntityManager(this.entities, connection, this.relations.all, this.indices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { test, describe, expect } from "bun:test";
|
import { test, describe, expect, beforeAll, afterAll } from "bun:test";
|
||||||
import * as q from "./query";
|
import * as q from "./query";
|
||||||
import { parse as $parse, type ParseOptions } from "bknd/utils";
|
import { parse as $parse, type ParseOptions } from "bknd/utils";
|
||||||
import type { PrimaryFieldType } from "modules";
|
import type { PrimaryFieldType } from "modules";
|
||||||
import type { Generated } from "kysely";
|
import type { Generated } from "kysely";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
const parse = (v: unknown, o: ParseOptions = {}) =>
|
const parse = (v: unknown, o: ParseOptions = {}) =>
|
||||||
$parse(q.repoQuery, v, {
|
$parse(q.repoQuery, v, {
|
||||||
@@ -15,6 +16,9 @@ const decode = (input: any, output: any) => {
|
|||||||
expect(parse(input)).toEqual(output);
|
expect(parse(input)).toEqual(output);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeAll(() => disableConsoleLog());
|
||||||
|
afterAll(() => enableConsoleLog());
|
||||||
|
|
||||||
describe("server/query", () => {
|
describe("server/query", () => {
|
||||||
test("limit & offset", () => {
|
test("limit & offset", () => {
|
||||||
//expect(() => parse({ limit: false })).toThrow();
|
//expect(() => parse({ limit: false })).toThrow();
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ export type * from "data/entities/Entity";
|
|||||||
export type { EntityManager } from "data/entities/EntityManager";
|
export type { EntityManager } from "data/entities/EntityManager";
|
||||||
export type { SchemaManager } from "data/schema/SchemaManager";
|
export type { SchemaManager } from "data/schema/SchemaManager";
|
||||||
export type * from "data/entities";
|
export type * from "data/entities";
|
||||||
|
|
||||||
|
// data connection
|
||||||
export {
|
export {
|
||||||
BaseIntrospector,
|
BaseIntrospector,
|
||||||
Connection,
|
Connection,
|
||||||
@@ -144,9 +146,29 @@ export {
|
|||||||
type ConnQuery,
|
type ConnQuery,
|
||||||
type ConnQueryResults,
|
type ConnQueryResults,
|
||||||
} from "data/connection";
|
} from "data/connection";
|
||||||
|
|
||||||
|
// data sqlite
|
||||||
export { SqliteConnection } from "data/connection/sqlite/SqliteConnection";
|
export { SqliteConnection } from "data/connection/sqlite/SqliteConnection";
|
||||||
export { SqliteIntrospector } from "data/connection/sqlite/SqliteIntrospector";
|
export { SqliteIntrospector } from "data/connection/sqlite/SqliteIntrospector";
|
||||||
export { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
export { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||||
|
|
||||||
|
// data postgres
|
||||||
|
export {
|
||||||
|
pg,
|
||||||
|
PgPostgresConnection,
|
||||||
|
} from "data/connection/postgres/PgPostgresConnection";
|
||||||
|
export { PostgresIntrospector } from "data/connection/postgres/PostgresIntrospector";
|
||||||
|
export { PostgresConnection } from "data/connection/postgres/PostgresConnection";
|
||||||
|
export {
|
||||||
|
postgresJs,
|
||||||
|
PostgresJsConnection,
|
||||||
|
} from "data/connection/postgres/PostgresJsConnection";
|
||||||
|
export {
|
||||||
|
createCustomPostgresConnection,
|
||||||
|
type CustomPostgresConnection,
|
||||||
|
} from "data/connection/postgres/custom";
|
||||||
|
|
||||||
|
// data prototype
|
||||||
export {
|
export {
|
||||||
text,
|
text,
|
||||||
number,
|
number,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export async function makeModeConfig<
|
|||||||
const { typesFilePath, configFilePath, writer, syncSecrets: syncSecretsOptions } = config;
|
const { typesFilePath, configFilePath, writer, syncSecrets: syncSecretsOptions } = config;
|
||||||
|
|
||||||
const isProd = config.isProduction ?? _isProd();
|
const isProd = config.isProduction ?? _isProd();
|
||||||
const plugins = appConfig?.options?.plugins ?? ([] as AppPlugin[]);
|
const plugins = config?.options?.plugins ?? ([] as AppPlugin[]);
|
||||||
const syncFallback = typeof config.syncSchema === "boolean" ? config.syncSchema : !isProd;
|
const syncFallback = typeof config.syncSchema === "boolean" ? config.syncSchema : !isProd;
|
||||||
const syncSchemaOptions =
|
const syncSchemaOptions =
|
||||||
typeof config.syncSchema === "object"
|
typeof config.syncSchema === "object"
|
||||||
|
|||||||
@@ -33,3 +33,5 @@ export const schemaRead = new Permission(
|
|||||||
);
|
);
|
||||||
export const build = new Permission("system.build");
|
export const build = new Permission("system.build");
|
||||||
export const mcp = new Permission("system.mcp");
|
export const mcp = new Permission("system.mcp");
|
||||||
|
export const info = new Permission("system.info");
|
||||||
|
export const openapi = new Permission("system.openapi");
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/// <reference types="@cloudflare/workers-types" />
|
|
||||||
|
|
||||||
import type { App } from "App";
|
import type { App } from "App";
|
||||||
import {
|
import {
|
||||||
datetimeStringLocal,
|
datetimeStringLocal,
|
||||||
@@ -125,7 +123,7 @@ export class SystemController extends Controller {
|
|||||||
private registerConfigController(client: Hono<any>): void {
|
private registerConfigController(client: Hono<any>): void {
|
||||||
const { permission } = this.middlewares;
|
const { permission } = this.middlewares;
|
||||||
// don't add auth again, it's already added in getController
|
// don't add auth again, it's already added in getController
|
||||||
const hono = this.create(); /* .use(permission(SystemPermissions.configRead)); */
|
const hono = this.create();
|
||||||
|
|
||||||
if (!this.app.isReadOnly()) {
|
if (!this.app.isReadOnly()) {
|
||||||
const manager = this.app.modules as DbModuleManager;
|
const manager = this.app.modules as DbModuleManager;
|
||||||
@@ -317,6 +315,11 @@ export class SystemController extends Controller {
|
|||||||
summary: "Get the config for a module",
|
summary: "Get the config for a module",
|
||||||
tags: ["system"],
|
tags: ["system"],
|
||||||
}),
|
}),
|
||||||
|
permission(SystemPermissions.configRead, {
|
||||||
|
context: (c) => ({
|
||||||
|
module: c.req.param("module"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
mcpTool("system_config", {
|
mcpTool("system_config", {
|
||||||
annotations: {
|
annotations: {
|
||||||
readOnlyHint: true,
|
readOnlyHint: true,
|
||||||
@@ -354,7 +357,7 @@ export class SystemController extends Controller {
|
|||||||
|
|
||||||
override getController() {
|
override getController() {
|
||||||
const { permission, auth } = this.middlewares;
|
const { permission, auth } = this.middlewares;
|
||||||
const hono = this.create().use(auth());
|
const hono = this.create().use(auth()).use(permission(SystemPermissions.accessApi, {}));
|
||||||
|
|
||||||
this.registerConfigController(hono);
|
this.registerConfigController(hono);
|
||||||
|
|
||||||
@@ -429,6 +432,9 @@ export class SystemController extends Controller {
|
|||||||
|
|
||||||
hono.get(
|
hono.get(
|
||||||
"/permissions",
|
"/permissions",
|
||||||
|
permission(SystemPermissions.schemaRead, {
|
||||||
|
context: (_c) => ({ module: "auth" }),
|
||||||
|
}),
|
||||||
describeRoute({
|
describeRoute({
|
||||||
summary: "Get the permissions",
|
summary: "Get the permissions",
|
||||||
tags: ["system"],
|
tags: ["system"],
|
||||||
@@ -441,6 +447,7 @@ export class SystemController extends Controller {
|
|||||||
|
|
||||||
hono.post(
|
hono.post(
|
||||||
"/build",
|
"/build",
|
||||||
|
permission(SystemPermissions.build, {}),
|
||||||
describeRoute({
|
describeRoute({
|
||||||
summary: "Build the app",
|
summary: "Build the app",
|
||||||
tags: ["system"],
|
tags: ["system"],
|
||||||
@@ -471,6 +478,7 @@ export class SystemController extends Controller {
|
|||||||
|
|
||||||
hono.get(
|
hono.get(
|
||||||
"/info",
|
"/info",
|
||||||
|
permission(SystemPermissions.info, {}),
|
||||||
mcpTool("system_info"),
|
mcpTool("system_info"),
|
||||||
describeRoute({
|
describeRoute({
|
||||||
summary: "Get the server info",
|
summary: "Get the server info",
|
||||||
@@ -504,6 +512,7 @@ export class SystemController extends Controller {
|
|||||||
|
|
||||||
hono.get(
|
hono.get(
|
||||||
"/openapi.json",
|
"/openapi.json",
|
||||||
|
permission(SystemPermissions.openapi, {}),
|
||||||
openAPISpecs(this.ctx.server, {
|
openAPISpecs(this.ctx.server, {
|
||||||
info: {
|
info: {
|
||||||
title: "bknd API",
|
title: "bknd API",
|
||||||
@@ -511,7 +520,11 @@ export class SystemController extends Controller {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
hono.get("/swagger", swaggerUI({ url: "/api/system/openapi.json" }));
|
hono.get(
|
||||||
|
"/swagger",
|
||||||
|
permission(SystemPermissions.openapi, {}),
|
||||||
|
swaggerUI({ url: "/api/system/openapi.json" }),
|
||||||
|
);
|
||||||
|
|
||||||
return hono;
|
return hono;
|
||||||
}
|
}
|
||||||
|
|||||||
683
app/src/plugins/auth/email-otp.plugin.spec.ts
Normal file
683
app/src/plugins/auth/email-otp.plugin.spec.ts
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, mock, test, setSystemTime } from "bun:test";
|
||||||
|
import { emailOTP } from "./email-otp.plugin";
|
||||||
|
import { createApp } from "core/test/utils";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
|
describe("otp plugin", () => {
|
||||||
|
test("should not work if auth is not enabled", async () => {
|
||||||
|
const app = createApp({
|
||||||
|
options: {
|
||||||
|
plugins: [emailOTP({ showActualErrors: true })],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should require email driver if sendEmail is true", async () => {
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [emailOTP()],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
|
||||||
|
{
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [emailOTP({ sendEmail: false })],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
const res = await app.server.request("/api/auth/otp/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should prevent mutations of the OTP entity", async () => {
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [emailOTP({ showActualErrors: true })],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
email: "test@test.com",
|
||||||
|
code: "123456",
|
||||||
|
action: "login",
|
||||||
|
created_at: new Date(),
|
||||||
|
expires_at: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
||||||
|
used_at: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(app.em.mutator("users_otp").insertOne(payload)).rejects.toThrow();
|
||||||
|
expect(
|
||||||
|
await app
|
||||||
|
.getApi()
|
||||||
|
.data.createOne("users_otp", payload)
|
||||||
|
.then((r) => r.ok),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should generate a token", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [emailOTP({ showActualErrors: true })],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async (to) => {
|
||||||
|
expect(to).toBe("test@test.com");
|
||||||
|
called();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seed: async (ctx) => {
|
||||||
|
await ctx.app.createUser({ email: "test@test.com", password: "12345678" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
const data = (await res.json()) as any;
|
||||||
|
expect(data.sent).toBe(true);
|
||||||
|
expect(data.data.email).toBe("test@test.com");
|
||||||
|
expect(data.data.action).toBe("login");
|
||||||
|
expect(data.data.expires_at).toBeDefined();
|
||||||
|
|
||||||
|
{
|
||||||
|
const { data } = await app.em.fork().repo("users_otp").findOne({ email: "test@test.com" });
|
||||||
|
expect(data?.code).toBeDefined();
|
||||||
|
expect(data?.code?.length).toBe(6);
|
||||||
|
expect(data?.code?.split("").every((char: string) => Number.isInteger(Number(char)))).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(data?.email).toBe("test@test.com");
|
||||||
|
}
|
||||||
|
expect(called).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should login with a code", async () => {
|
||||||
|
let code = "";
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
emailOTP({
|
||||||
|
showActualErrors: true,
|
||||||
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async (to, _subject, body) => {
|
||||||
|
expect(to).toBe("test@test.com");
|
||||||
|
code = String(body);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seed: async (ctx) => {
|
||||||
|
await ctx.app.createUser({ email: "test@test.com", password: "12345678" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.headers.get("set-cookie")).toBeDefined();
|
||||||
|
const userData = (await res.json()) as any;
|
||||||
|
expect(userData.user.email).toBe("test@test.com");
|
||||||
|
expect(userData.token).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should register with a code", async () => {
|
||||||
|
let code = "";
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
emailOTP({
|
||||||
|
showActualErrors: true,
|
||||||
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async (to, _subject, body) => {
|
||||||
|
expect(to).toBe("test@test.com");
|
||||||
|
code = String(body);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
const res = await app.server.request("/api/auth/otp/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
const data = (await res.json()) as any;
|
||||||
|
expect(data.sent).toBe(true);
|
||||||
|
expect(data.data.email).toBe("test@test.com");
|
||||||
|
expect(data.data.action).toBe("register");
|
||||||
|
expect(data.data.expires_at).toBeDefined();
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await app.server.request("/api/auth/otp/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.headers.get("set-cookie")).toBeDefined();
|
||||||
|
const userData = (await res.json()) as any;
|
||||||
|
expect(userData.user.email).toBe("test@test.com");
|
||||||
|
expect(userData.token).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not send email if sendEmail is false", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [emailOTP({ sendEmail: false })],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async () => {
|
||||||
|
called();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
const res = await app.server.request("/api/auth/otp/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
expect(called).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject invalid codes", async () => {
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
emailOTP({
|
||||||
|
showActualErrors: true,
|
||||||
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seed: async (ctx) => {
|
||||||
|
await ctx.app.createUser({ email: "test@test.com", password: "12345678" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
// First send a code
|
||||||
|
await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to use an invalid code
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code: "999999" }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const error = await res.json();
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject code reuse", async () => {
|
||||||
|
let code = "";
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
emailOTP({
|
||||||
|
showActualErrors: true,
|
||||||
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async (_to, _subject, body) => {
|
||||||
|
code = String(body);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seed: async (ctx) => {
|
||||||
|
await ctx.app.createUser({ email: "test@test.com", password: "12345678" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
// Send a code
|
||||||
|
await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use the code successfully
|
||||||
|
{
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to use the same code again
|
||||||
|
{
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const error = await res.json();
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject expired codes", async () => {
|
||||||
|
// Set a fixed system time
|
||||||
|
const baseTime = Date.now();
|
||||||
|
setSystemTime(new Date(baseTime));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
emailOTP({
|
||||||
|
showActualErrors: true,
|
||||||
|
ttl: 1, // 1 second TTL
|
||||||
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seed: async (ctx) => {
|
||||||
|
await ctx.app.createUser({ email: "test@test.com", password: "12345678" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
// Send a code
|
||||||
|
const sendRes = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
expect(sendRes.status).toBe(201);
|
||||||
|
|
||||||
|
// Get the code from the database
|
||||||
|
const { data: otpData } = await app.em
|
||||||
|
.fork()
|
||||||
|
.repo("users_otp")
|
||||||
|
.findOne({ email: "test@test.com" });
|
||||||
|
expect(otpData?.code).toBeDefined();
|
||||||
|
|
||||||
|
// Advance system time by more than 1 second to expire the code
|
||||||
|
setSystemTime(new Date(baseTime + 1100));
|
||||||
|
|
||||||
|
// Try to use the expired code
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code: otpData?.code }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const error = await res.json();
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
} finally {
|
||||||
|
// Reset system time
|
||||||
|
setSystemTime();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject codes with different actions", async () => {
|
||||||
|
let loginCode = "";
|
||||||
|
let registerCode = "";
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
emailOTP({
|
||||||
|
showActualErrors: true,
|
||||||
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seed: async (ctx) => {
|
||||||
|
await ctx.app.createUser({ email: "test@test.com", password: "12345678" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
|
||||||
|
// Send a login code
|
||||||
|
await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the login code
|
||||||
|
const { data: loginOtp } = await app
|
||||||
|
.getApi()
|
||||||
|
.data.readOneBy("users_otp", { where: { email: "test@test.com", action: "login" } });
|
||||||
|
loginCode = loginOtp?.code || "";
|
||||||
|
|
||||||
|
// Send a register code
|
||||||
|
await app.server.request("/api/auth/otp/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the register code
|
||||||
|
const { data: registerOtp } = await app
|
||||||
|
.getApi()
|
||||||
|
.data.readOneBy("users_otp", { where: { email: "test@test.com", action: "register" } });
|
||||||
|
registerCode = registerOtp?.code || "";
|
||||||
|
|
||||||
|
// Try to use login code for register
|
||||||
|
{
|
||||||
|
const res = await app.server.request("/api/auth/otp/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code: loginCode }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const error = await res.json();
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to use register code for login
|
||||||
|
{
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code: registerCode }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const error = await res.json();
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should invalidate previous codes when sending new code", async () => {
|
||||||
|
let firstCode = "";
|
||||||
|
let secondCode = "";
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
secret: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
emailOTP({
|
||||||
|
showActualErrors: true,
|
||||||
|
generateEmail: (otp) => ({ subject: "test", body: otp.code }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
drivers: {
|
||||||
|
email: {
|
||||||
|
send: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seed: async (ctx) => {
|
||||||
|
await ctx.app.createUser({ email: "test@test.com", password: "12345678" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.build();
|
||||||
|
const em = app.em.fork();
|
||||||
|
|
||||||
|
// Send first code
|
||||||
|
await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the first code
|
||||||
|
const { data: firstOtp } = await em
|
||||||
|
.repo("users_otp")
|
||||||
|
.findOne({ email: "test@test.com", action: "login" });
|
||||||
|
firstCode = firstOtp?.code || "";
|
||||||
|
expect(firstCode).toBeDefined();
|
||||||
|
|
||||||
|
// Send second code (should invalidate the first)
|
||||||
|
await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the second code
|
||||||
|
const { data: secondOtp } = await em
|
||||||
|
.repo("users_otp")
|
||||||
|
.findOne({ email: "test@test.com", action: "login" });
|
||||||
|
secondCode = secondOtp?.code || "";
|
||||||
|
expect(secondCode).toBeDefined();
|
||||||
|
expect(secondCode).not.toBe(firstCode);
|
||||||
|
|
||||||
|
// Try to use the first code (should fail as it's been invalidated)
|
||||||
|
{
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code: firstCode }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const error = await res.json();
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The second code should work
|
||||||
|
{
|
||||||
|
const res = await app.server.request("/api/auth/otp/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: "test@test.com", code: secondCode }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
387
app/src/plugins/auth/email-otp.plugin.ts
Normal file
387
app/src/plugins/auth/email-otp.plugin.ts
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
import {
|
||||||
|
datetime,
|
||||||
|
em,
|
||||||
|
entity,
|
||||||
|
enumm,
|
||||||
|
Exception,
|
||||||
|
text,
|
||||||
|
type App,
|
||||||
|
type AppPlugin,
|
||||||
|
type DB,
|
||||||
|
type FieldSchema,
|
||||||
|
type MaybePromise,
|
||||||
|
type EntityConfig,
|
||||||
|
DatabaseEvents,
|
||||||
|
} from "bknd";
|
||||||
|
import {
|
||||||
|
invariant,
|
||||||
|
s,
|
||||||
|
jsc,
|
||||||
|
HttpStatus,
|
||||||
|
threwAsync,
|
||||||
|
randomString,
|
||||||
|
$console,
|
||||||
|
pickKeys,
|
||||||
|
} from "bknd/utils";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
|
||||||
|
export type EmailOTPPluginOptions = {
|
||||||
|
/**
|
||||||
|
* Customize code generation. If not provided, a random 6-digit code will be generated.
|
||||||
|
*/
|
||||||
|
generateCode?: (user: Pick<DB["users"], "email">) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base path for the API endpoints.
|
||||||
|
* @default "/api/auth/otp"
|
||||||
|
*/
|
||||||
|
apiBasePath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The TTL for the OTP tokens in seconds.
|
||||||
|
* @default 600 (10 minutes)
|
||||||
|
*/
|
||||||
|
ttl?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the OTP entity.
|
||||||
|
* @default "users_otp"
|
||||||
|
*/
|
||||||
|
entity?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The config for the OTP entity.
|
||||||
|
*/
|
||||||
|
entityConfig?: EntityConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customize email content. If not provided, a default email will be sent.
|
||||||
|
*/
|
||||||
|
generateEmail?: (
|
||||||
|
otp: EmailOTPFieldSchema,
|
||||||
|
) => MaybePromise<{ subject: string; body: string | { text: string; html: string } }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable debug mode for error messages.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
showActualErrors?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow direct mutations (create/update) of OTP codes outside of this plugin,
|
||||||
|
* e.g. via API or admin UI. If false, mutations are only allowed via the plugin's flows.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
allowExternalMutations?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to send the email with the OTP code.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
sendEmail?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const otpFields = {
|
||||||
|
action: enumm({
|
||||||
|
enum: ["login", "register"],
|
||||||
|
}),
|
||||||
|
code: text().required(),
|
||||||
|
email: text().required(),
|
||||||
|
created_at: datetime(),
|
||||||
|
expires_at: datetime().required(),
|
||||||
|
used_at: datetime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EmailOTPFieldSchema = FieldSchema<typeof otpFields>;
|
||||||
|
|
||||||
|
class OTPError extends Exception {
|
||||||
|
override name = "OTPError";
|
||||||
|
override code = HttpStatus.BAD_REQUEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emailOTP({
|
||||||
|
generateCode: _generateCode,
|
||||||
|
apiBasePath = "/api/auth/otp",
|
||||||
|
ttl = 600,
|
||||||
|
entity: entityName = "users_otp",
|
||||||
|
entityConfig,
|
||||||
|
generateEmail: _generateEmail,
|
||||||
|
showActualErrors = false,
|
||||||
|
allowExternalMutations = false,
|
||||||
|
sendEmail = true,
|
||||||
|
}: EmailOTPPluginOptions = {}): AppPlugin {
|
||||||
|
return (app: App) => {
|
||||||
|
return {
|
||||||
|
name: "email-otp",
|
||||||
|
schema: () =>
|
||||||
|
em(
|
||||||
|
{
|
||||||
|
[entityName]: entity(
|
||||||
|
entityName,
|
||||||
|
otpFields,
|
||||||
|
{
|
||||||
|
name: "Users OTP",
|
||||||
|
sort_dir: "desc",
|
||||||
|
primary_format: app.module.data.config.default_primary_format,
|
||||||
|
...entityConfig,
|
||||||
|
},
|
||||||
|
"generated",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
({ index }, schema) => {
|
||||||
|
const otp = schema[entityName]!;
|
||||||
|
index(otp).on(["email", "expires_at", "code"]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onBuilt: async () => {
|
||||||
|
const auth = app.module.auth;
|
||||||
|
invariant(auth && auth.enabled === true, "Auth is not enabled");
|
||||||
|
invariant(!sendEmail || app.drivers?.email, "Email driver is not registered");
|
||||||
|
|
||||||
|
const generateCode =
|
||||||
|
_generateCode ?? (() => Math.floor(100000 + Math.random() * 900000).toString());
|
||||||
|
const generateEmail =
|
||||||
|
_generateEmail ??
|
||||||
|
((otp: EmailOTPFieldSchema) => ({
|
||||||
|
subject: "OTP Code",
|
||||||
|
body: `Your OTP code is: ${otp.code}`,
|
||||||
|
}));
|
||||||
|
const em = app.em.fork();
|
||||||
|
|
||||||
|
const hono = new Hono()
|
||||||
|
.post(
|
||||||
|
"/login",
|
||||||
|
jsc(
|
||||||
|
"json",
|
||||||
|
s.object({
|
||||||
|
email: s.string({ format: "email" }),
|
||||||
|
code: s.string({ minLength: 1 }).optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
jsc("query", s.object({ redirect: s.string().optional() })),
|
||||||
|
async (c) => {
|
||||||
|
const { email, code } = c.req.valid("json");
|
||||||
|
const { redirect } = c.req.valid("query");
|
||||||
|
const user = await findUser(app, email);
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
const otpData = await getValidatedCode(
|
||||||
|
app,
|
||||||
|
entityName,
|
||||||
|
email,
|
||||||
|
code,
|
||||||
|
"login",
|
||||||
|
);
|
||||||
|
await em.mutator(entityName).updateOne(otpData.id, { used_at: new Date() });
|
||||||
|
|
||||||
|
const jwt = await auth.authenticator.jwt(user);
|
||||||
|
// @ts-expect-error private method
|
||||||
|
return auth.authenticator.respondWithUser(
|
||||||
|
c,
|
||||||
|
{ user, token: jwt },
|
||||||
|
{ redirect },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const otpData = await invalidateAndGenerateCode(
|
||||||
|
app,
|
||||||
|
{ generateCode, ttl, entity: entityName },
|
||||||
|
user,
|
||||||
|
"login",
|
||||||
|
);
|
||||||
|
if (sendEmail) {
|
||||||
|
await sendCode(app, otpData, { generateEmail });
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
sent: true,
|
||||||
|
data: pickKeys(otpData, ["email", "action", "expires_at"]),
|
||||||
|
},
|
||||||
|
HttpStatus.CREATED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/register",
|
||||||
|
jsc(
|
||||||
|
"json",
|
||||||
|
s.object({
|
||||||
|
email: s.string({ format: "email" }),
|
||||||
|
code: s.string({ minLength: 1 }).optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
jsc("query", s.object({ redirect: s.string().optional() })),
|
||||||
|
async (c) => {
|
||||||
|
const { email, code } = c.req.valid("json");
|
||||||
|
const { redirect } = c.req.valid("query");
|
||||||
|
|
||||||
|
// throw if user exists
|
||||||
|
if (!(await threwAsync(findUser(app, email)))) {
|
||||||
|
throw new Exception("User already exists", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
const otpData = await getValidatedCode(
|
||||||
|
app,
|
||||||
|
entityName,
|
||||||
|
email,
|
||||||
|
code,
|
||||||
|
"register",
|
||||||
|
);
|
||||||
|
await em.mutator(entityName).updateOne(otpData.id, { used_at: new Date() });
|
||||||
|
|
||||||
|
const user = await app.createUser({
|
||||||
|
email,
|
||||||
|
password: randomString(32, true),
|
||||||
|
});
|
||||||
|
|
||||||
|
const jwt = await auth.authenticator.jwt(user);
|
||||||
|
// @ts-expect-error private method
|
||||||
|
return auth.authenticator.respondWithUser(
|
||||||
|
c,
|
||||||
|
{ user, token: jwt },
|
||||||
|
{ redirect },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const otpData = await invalidateAndGenerateCode(
|
||||||
|
app,
|
||||||
|
{ generateCode, ttl, entity: entityName },
|
||||||
|
{ email },
|
||||||
|
"register",
|
||||||
|
);
|
||||||
|
if (sendEmail) {
|
||||||
|
await sendCode(app, otpData, { generateEmail });
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
sent: true,
|
||||||
|
data: pickKeys(otpData, ["email", "action", "expires_at"]),
|
||||||
|
},
|
||||||
|
HttpStatus.CREATED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.onError((err) => {
|
||||||
|
if (showActualErrors || err instanceof OTPError) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Invalid credentials", HttpStatus.BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.server.route(apiBasePath, hono);
|
||||||
|
|
||||||
|
if (allowExternalMutations !== true) {
|
||||||
|
registerListeners(app, entityName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findUser(app: App, email: string) {
|
||||||
|
const user_entity = app.module.auth.config.entity_name as "users";
|
||||||
|
const { data: user } = await app.em.repo(user_entity).findOne({ email });
|
||||||
|
if (!user) {
|
||||||
|
throw new Exception("User not found", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invalidateAndGenerateCode(
|
||||||
|
app: App,
|
||||||
|
opts: Required<Pick<EmailOTPPluginOptions, "generateCode" | "ttl" | "entity">>,
|
||||||
|
user: Pick<DB["users"], "email">,
|
||||||
|
action: EmailOTPFieldSchema["action"],
|
||||||
|
) {
|
||||||
|
const { generateCode, ttl, entity: entityName } = opts;
|
||||||
|
const newCode = generateCode?.(user);
|
||||||
|
if (!newCode) {
|
||||||
|
throw new OTPError("Failed to generate code");
|
||||||
|
}
|
||||||
|
|
||||||
|
await invalidateAllUserCodes(app, entityName, user.email, ttl);
|
||||||
|
const { data: otpData } = await app.em
|
||||||
|
.fork()
|
||||||
|
.mutator(entityName)
|
||||||
|
.insertOne({
|
||||||
|
code: newCode,
|
||||||
|
email: user.email,
|
||||||
|
action,
|
||||||
|
created_at: new Date(),
|
||||||
|
expires_at: new Date(Date.now() + ttl * 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
$console.log("[OTP Code]", newCode);
|
||||||
|
|
||||||
|
return otpData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendCode(
|
||||||
|
app: App,
|
||||||
|
otpData: EmailOTPFieldSchema,
|
||||||
|
opts: Required<Pick<EmailOTPPluginOptions, "generateEmail">>,
|
||||||
|
) {
|
||||||
|
const { generateEmail } = opts;
|
||||||
|
const { subject, body } = await generateEmail(otpData);
|
||||||
|
await app.drivers?.email?.send(otpData.email, subject, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getValidatedCode(
|
||||||
|
app: App,
|
||||||
|
entityName: string,
|
||||||
|
email: string,
|
||||||
|
code: string,
|
||||||
|
action: EmailOTPFieldSchema["action"],
|
||||||
|
) {
|
||||||
|
invariant(email, "[OTP Plugin]: Email is required");
|
||||||
|
invariant(code, "[OTP Plugin]: Code is required");
|
||||||
|
const em = app.em.fork();
|
||||||
|
const { data: otpData } = await em.repo(entityName).findOne({ email, code, action });
|
||||||
|
if (!otpData) {
|
||||||
|
throw new OTPError("Invalid code");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otpData.expires_at < new Date()) {
|
||||||
|
throw new OTPError("Code expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otpData.used_at) {
|
||||||
|
throw new OTPError("Code already used");
|
||||||
|
}
|
||||||
|
|
||||||
|
return otpData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invalidateAllUserCodes(app: App, entityName: string, email: string, ttl: number) {
|
||||||
|
invariant(ttl > 0, "[OTP Plugin]: TTL must be greater than 0");
|
||||||
|
invariant(email, "[OTP Plugin]: Email is required");
|
||||||
|
const em = app.em.fork();
|
||||||
|
await em
|
||||||
|
.mutator(entityName)
|
||||||
|
.updateWhere(
|
||||||
|
{ expires_at: new Date(Date.now() - 1000) },
|
||||||
|
{ email, used_at: { $isnull: true } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerListeners(app: App, entityName: string) {
|
||||||
|
[DatabaseEvents.MutatorInsertBefore, DatabaseEvents.MutatorUpdateBefore].forEach((event) => {
|
||||||
|
app.emgr.onEvent(
|
||||||
|
event,
|
||||||
|
(e: { params: { entity: { name: string } } }) => {
|
||||||
|
if (e.params.entity.name === entityName) {
|
||||||
|
throw new OTPError("Mutations of the OTP entity are not allowed");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: "sync",
|
||||||
|
id: "bknd-email-otp",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,3 +8,4 @@ export { syncConfig, type SyncConfigOptions } from "./dev/sync-config.plugin";
|
|||||||
export { syncTypes, type SyncTypesOptions } from "./dev/sync-types.plugin";
|
export { syncTypes, type SyncTypesOptions } from "./dev/sync-types.plugin";
|
||||||
export { syncSecrets, type SyncSecretsOptions } from "./dev/sync-secrets.plugin";
|
export { syncSecrets, type SyncSecretsOptions } from "./dev/sync-secrets.plugin";
|
||||||
export { timestamps, type TimestampsPluginOptions } from "./data/timestamps.plugin";
|
export { timestamps, type TimestampsPluginOptions } from "./data/timestamps.plugin";
|
||||||
|
export { emailOTP, type EmailOTPPluginOptions } from "./auth/email-otp.plugin";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useApi } from "ui/client";
|
import { useApi } from "bknd/client";
|
||||||
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
||||||
import { AppReduced } from "./utils/AppReduced";
|
import { AppReduced } from "./utils/AppReduced";
|
||||||
import { Message } from "ui/components/display/Message";
|
import { Message } from "ui/components/display/Message";
|
||||||
|
|||||||
@@ -14,18 +14,20 @@ const ClientContext = createContext<BkndClientContext>(undefined!);
|
|||||||
export type ClientProviderProps = {
|
export type ClientProviderProps = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
|
api?: Api;
|
||||||
} & ApiOptions;
|
} & ApiOptions;
|
||||||
|
|
||||||
export const ClientProvider = ({
|
export const ClientProvider = ({
|
||||||
children,
|
children,
|
||||||
host,
|
host,
|
||||||
baseUrl: _baseUrl = host,
|
baseUrl: _baseUrl = host,
|
||||||
|
api: _api,
|
||||||
...props
|
...props
|
||||||
}: ClientProviderProps) => {
|
}: ClientProviderProps) => {
|
||||||
const winCtx = useBkndWindowContext();
|
const winCtx = useBkndWindowContext();
|
||||||
const _ctx = useClientContext();
|
const _ctx = useClientContext();
|
||||||
let actualBaseUrl = _baseUrl ?? _ctx?.baseUrl ?? "";
|
let actualBaseUrl = _baseUrl ?? _ctx?.baseUrl ?? "";
|
||||||
let user: any = undefined;
|
let user: any;
|
||||||
|
|
||||||
if (winCtx) {
|
if (winCtx) {
|
||||||
user = winCtx.user;
|
user = winCtx.user;
|
||||||
@@ -40,6 +42,7 @@ export const ClientProvider = ({
|
|||||||
const apiProps = { user, ...props, host: actualBaseUrl };
|
const apiProps = { user, ...props, host: actualBaseUrl };
|
||||||
const api = useMemo(
|
const api = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
_api ??
|
||||||
new Api({
|
new Api({
|
||||||
...apiProps,
|
...apiProps,
|
||||||
verbose: isDebug(),
|
verbose: isDebug(),
|
||||||
@@ -50,7 +53,7 @@ export const ClientProvider = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[JSON.stringify(apiProps)],
|
[_api, JSON.stringify(apiProps)],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [authState, setAuthState] = useState<Partial<AuthState> | undefined>(api.getAuthState());
|
const [authState, setAuthState] = useState<Partial<AuthState> | undefined>(api.getAuthState());
|
||||||
@@ -64,6 +67,10 @@ export const ClientProvider = ({
|
|||||||
|
|
||||||
export const useApi = (host?: ApiOptions["host"]): Api => {
|
export const useApi = (host?: ApiOptions["host"]): Api => {
|
||||||
const context = useContext(ClientContext);
|
const context = useContext(ClientContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useApi must be used within a ClientProvider");
|
||||||
|
}
|
||||||
|
|
||||||
if (!context?.api || (host && host.length > 0 && host !== context.baseUrl)) {
|
if (!context?.api || (host && host.length > 0 && host !== context.baseUrl)) {
|
||||||
return new Api({ host: host ?? "" });
|
return new Api({ host: host ?? "" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Api } from "Api";
|
|||||||
import { FetchPromise, type ModuleApi, type ResponseObject } from "modules/ModuleApi";
|
import { FetchPromise, type ModuleApi, type ResponseObject } from "modules/ModuleApi";
|
||||||
import useSWR, { type SWRConfiguration, useSWRConfig, type Middleware, type SWRHook } from "swr";
|
import useSWR, { type SWRConfiguration, useSWRConfig, type Middleware, type SWRHook } from "swr";
|
||||||
import useSWRInfinite from "swr/infinite";
|
import useSWRInfinite from "swr/infinite";
|
||||||
import { useApi } from "ui/client";
|
import { useApi } from "../ClientProvider";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const useApiQuery = <
|
export const useApiQuery = <
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
import { objectTransform, encodeSearch } from "bknd/utils";
|
import { objectTransform, encodeSearch } from "bknd/utils";
|
||||||
import type { Insertable, Selectable, Updateable, Generated } from "kysely";
|
import type { Insertable, Selectable, Updateable, Generated } from "kysely";
|
||||||
import useSWR, { type SWRConfiguration, type SWRResponse, mutate } from "swr";
|
import useSWR, { type SWRConfiguration, type SWRResponse, mutate } from "swr";
|
||||||
import { type Api, useApi } from "ui/client";
|
import { type Api, useApi } from "bknd/client";
|
||||||
|
|
||||||
export class UseEntityApiError<Payload = any> extends Error {
|
export class UseEntityApiError<Payload = any> extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export {
|
|||||||
type ClientProviderProps,
|
type ClientProviderProps,
|
||||||
useApi,
|
useApi,
|
||||||
useBaseUrl,
|
useBaseUrl,
|
||||||
|
useClientContext
|
||||||
} from "./ClientProvider";
|
} from "./ClientProvider";
|
||||||
|
|
||||||
export * from "./api/use-api";
|
export * from "./api/use-api";
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { AuthState } from "Api";
|
import type { AuthState } from "Api";
|
||||||
import type { AuthResponse } from "bknd";
|
import type { AuthResponse } from "bknd";
|
||||||
import { useApi, useInvalidate } from "ui/client";
|
import { useApi, useInvalidate, useClientContext } from "bknd/client";
|
||||||
import { useClientContext } from "ui/client/ClientProvider";
|
|
||||||
|
|
||||||
type LoginData = {
|
type LoginData = {
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { AppAuthSchema } from "auth/auth-schema";
|
import type { AppAuthSchema } from "auth/auth-schema";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useApi } from "ui/client";
|
import { useApi } from "bknd/client";
|
||||||
|
|
||||||
type AuthStrategyData = Pick<AppAuthSchema, "strategies" | "basepath">;
|
type AuthStrategyData = Pick<AppAuthSchema, "strategies" | "basepath">;
|
||||||
export const useAuthStrategies = (options?: { baseUrl?: string }): Partial<AuthStrategyData> & {
|
export const useAuthStrategies = (options?: {
|
||||||
|
baseUrl?: string;
|
||||||
|
}): Partial<AuthStrategyData> & {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
} => {
|
} => {
|
||||||
const [data, setData] = useState<AuthStrategyData>();
|
const [data, setData] = useState<AuthStrategyData>();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { isFileAccepted } from "bknd/utils";
|
|||||||
import { type FileWithPath, useDropzone } from "./use-dropzone";
|
import { type FileWithPath, useDropzone } from "./use-dropzone";
|
||||||
import { checkMaxReached } from "./helper";
|
import { checkMaxReached } from "./helper";
|
||||||
import { DropzoneInner } from "./DropzoneInner";
|
import { DropzoneInner } from "./DropzoneInner";
|
||||||
import { createDropzoneStore } from "ui/elements/media/dropzone-state";
|
import { createDropzoneStore } from "./dropzone-state";
|
||||||
import { useStore } from "zustand";
|
import { useStore } from "zustand";
|
||||||
|
|
||||||
export type FileState = {
|
export type FileState = {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { Api } from "bknd/client";
|
|
||||||
import type { PrimaryFieldType, RepoQueryIn } from "bknd";
|
import type { PrimaryFieldType, RepoQueryIn } from "bknd";
|
||||||
import type { MediaFieldSchema } from "media/AppMedia";
|
import type { MediaFieldSchema } from "media/AppMedia";
|
||||||
import type { TAppMediaConfig } from "media/media-schema";
|
import type { TAppMediaConfig } from "media/media-schema";
|
||||||
import { useId, useEffect, useRef, useState } from "react";
|
import { useId, useEffect, useRef, useState } from "react";
|
||||||
import { useApi, useApiInfiniteQuery, useApiQuery, useInvalidate } from "bknd/client";
|
import { type Api, useApi, useApiInfiniteQuery, useApiQuery, useInvalidate } from "bknd/client";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { Dropzone, type DropzoneProps } from "./Dropzone";
|
import { Dropzone, type DropzoneProps } from "./Dropzone";
|
||||||
import { mediaItemsToFileStates } from "./helper";
|
import { mediaItemsToFileStates } from "./helper";
|
||||||
@@ -132,26 +131,24 @@ export function DropzoneContainer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dropzone
|
||||||
<Dropzone
|
key={key}
|
||||||
key={key}
|
getUploadInfo={getUploadInfo}
|
||||||
getUploadInfo={getUploadInfo}
|
handleDelete={handleDelete}
|
||||||
handleDelete={handleDelete}
|
autoUpload
|
||||||
autoUpload
|
initialItems={_initialItems}
|
||||||
initialItems={_initialItems}
|
footer={
|
||||||
footer={
|
infinite &&
|
||||||
infinite &&
|
"setSize" in $q && (
|
||||||
"setSize" in $q && (
|
<Footer
|
||||||
<Footer
|
items={_initialItems.length}
|
||||||
items={_initialItems.length}
|
length={placeholderLength}
|
||||||
length={placeholderLength}
|
onFirstVisible={() => $q.setSize($q.size + 1)}
|
||||||
onFirstVisible={() => $q.setSize($q.size + 1)}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import {
|
|||||||
} from "react-icons/tb";
|
} from "react-icons/tb";
|
||||||
import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown";
|
import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { formatNumber } from "core/utils";
|
import { formatNumber } from "bknd/utils";
|
||||||
import type { DropzoneRenderProps, FileState } from "ui/elements";
|
import type { DropzoneRenderProps, FileState } from "./Dropzone";
|
||||||
import { useDropzoneFileState, useDropzoneState } from "./Dropzone";
|
import { useDropzoneFileState, useDropzoneState } from "./Dropzone";
|
||||||
|
|
||||||
function handleUploadError(e: unknown) {
|
function handleUploadError(e: unknown) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
TbUser,
|
TbUser,
|
||||||
TbX,
|
TbX,
|
||||||
} from "react-icons/tb";
|
} from "react-icons/tb";
|
||||||
import { useAuth, useBkndWindowContext } from "ui/client";
|
import { useAuth, useBkndWindowContext } from "bknd/client";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { useTheme } from "ui/client/use-theme";
|
import { useTheme } from "ui/client/use-theme";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ContextModalProps } from "@mantine/modals";
|
import type { ContextModalProps } from "@mantine/modals";
|
||||||
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
||||||
import { useEntityQuery } from "ui/client";
|
import { useEntityQuery } from "bknd/client";
|
||||||
import { type FileState, Media } from "ui/elements";
|
import { type FileState, Media } from "ui/elements";
|
||||||
import { autoFormatString, datetimeStringLocal, formatNumber } from "core/utils";
|
import { autoFormatString, datetimeStringLocal, formatNumber } from "core/utils";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useApi, useInvalidate } from "ui/client";
|
import { useApi, useInvalidate } from "bknd/client";
|
||||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||||
import { routes, useNavigate } from "ui/lib/routes";
|
import { routes, useNavigate } from "ui/lib/routes";
|
||||||
import { bkndModals } from "ui/modals";
|
import { bkndModals } from "ui/modals";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { EntityData } from "bknd";
|
|||||||
import type { RelationField } from "data/relations";
|
import type { RelationField } from "data/relations";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { TbEye } from "react-icons/tb";
|
import { TbEye } from "react-icons/tb";
|
||||||
import { useEntityQuery } from "ui/client";
|
import { useEntityQuery } from "bknd/client";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { TbArrowRight, TbCircle, TbCircleCheckFilled, TbFingerprint } from "react-icons/tb";
|
import { TbArrowRight, TbCircle, TbCircleCheckFilled, TbFingerprint } from "react-icons/tb";
|
||||||
import { useApiQuery } from "ui/client";
|
import { useApiQuery } from "bknd/client";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
|
||||||
import { ButtonLink, type ButtonLinkProps } from "ui/components/buttons/Button";
|
import { ButtonLink, type ButtonLinkProps } from "ui/components/buttons/Button";
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { SegmentedControl, Tooltip } from "@mantine/core";
|
|||||||
import { Popover } from "ui/components/overlay/Popover";
|
import { Popover } from "ui/components/overlay/Popover";
|
||||||
import { cn } from "ui/lib/utils";
|
import { cn } from "ui/lib/utils";
|
||||||
import { JsonViewer } from "ui/components/code/JsonViewer";
|
import { JsonViewer } from "ui/components/code/JsonViewer";
|
||||||
import { mountOnce, useApiQuery } from "ui/client";
|
import { mountOnce, useApiQuery } from "bknd/client";
|
||||||
import { CodePreview } from "ui/components/code/CodePreview";
|
import { CodePreview } from "ui/components/code/CodePreview";
|
||||||
import type { JsonError } from "json-schema-library";
|
import type { JsonError } from "json-schema-library";
|
||||||
import { Alert } from "ui/components/display/Alert";
|
import { Alert } from "ui/components/display/Alert";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ucFirst } from "bknd/utils";
|
|||||||
import type { Entity, EntityData, EntityRelation } from "bknd";
|
import type { Entity, EntityData, EntityRelation } from "bknd";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { TbDots } from "react-icons/tb";
|
import { TbDots } from "react-icons/tb";
|
||||||
import { useApiQuery, useEntityQuery } from "ui/client";
|
import { useApiQuery, useEntityQuery } from "bknd/client";
|
||||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { EntityData } from "bknd";
|
import type { EntityData } from "bknd";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useEntityMutate } from "ui/client";
|
import { useEntityMutate } from "bknd/client";
|
||||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { Message } from "ui/components/display/Message";
|
import { Message } from "ui/components/display/Message";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Entity } from "bknd";
|
|||||||
import { repoQuery } from "data/server/query";
|
import { repoQuery } from "data/server/query";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { TbDots } from "react-icons/tb";
|
import { TbDots } from "react-icons/tb";
|
||||||
import { useApiQuery } from "ui/client";
|
import { useApiQuery } from "bknd/client";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import {
|
|||||||
ucFirstAllSnakeToPascalWithSpaces,
|
ucFirstAllSnakeToPascalWithSpaces,
|
||||||
s,
|
s,
|
||||||
stringIdentifier,
|
stringIdentifier,
|
||||||
|
pickKeys,
|
||||||
} from "bknd/utils";
|
} from "bknd/utils";
|
||||||
import {
|
import {
|
||||||
type TAppDataEntityFields,
|
type TAppDataEntityFields,
|
||||||
fieldsSchemaObject as originalFieldsSchemaObject,
|
fieldsSchemaObject as originalFieldsSchemaObject,
|
||||||
} from "data/data-schema";
|
} from "data/data-schema";
|
||||||
import { omit } from "lodash-es";
|
|
||||||
import { forwardRef, memo, useEffect, useImperativeHandle } from "react";
|
import { forwardRef, memo, useEffect, useImperativeHandle } from "react";
|
||||||
import { type FieldArrayWithId, type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
|
import { type FieldArrayWithId, type UseFormReturn, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { TbGripVertical, TbSettings, TbTrash } from "react-icons/tb";
|
import { TbGripVertical, TbSettings, TbTrash } from "react-icons/tb";
|
||||||
@@ -317,7 +317,6 @@ function EntityField({
|
|||||||
const name = watch(`fields.${index}.name`);
|
const name = watch(`fields.${index}.name`);
|
||||||
const { active, toggle } = useRoutePathState(routePattern ?? "", name);
|
const { active, toggle } = useRoutePathState(routePattern ?? "", name);
|
||||||
const fieldSpec = fieldSpecs.find((s) => s.type === type)!;
|
const fieldSpec = fieldSpecs.find((s) => s.type === type)!;
|
||||||
const specificData = omit(field.field.config, commonProps);
|
|
||||||
const disabled = fieldSpec.disabled || [];
|
const disabled = fieldSpec.disabled || [];
|
||||||
const hidden = fieldSpec.hidden || [];
|
const hidden = fieldSpec.hidden || [];
|
||||||
const dragDisabled = index === 0;
|
const dragDisabled = index === 0;
|
||||||
@@ -476,7 +475,7 @@ function EntityField({
|
|||||||
field={field}
|
field={field}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setValue(`${prefix}.config`, {
|
setValue(`${prefix}.config`, {
|
||||||
...getValues([`fields.${index}.config`])[0],
|
...pickKeys(getValues([`${prefix}.config`])[0], commonProps),
|
||||||
...value,
|
...value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -520,7 +519,7 @@ const SpecificForm = ({
|
|||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const type = field.field.type;
|
const type = field.field.type;
|
||||||
const specificData = omit(field.field.config, commonProps);
|
const specificData = omitKeys(field.field.config ?? {}, commonProps);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JsonSchemaForm
|
<JsonSchemaForm
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import SettingsRoutes from "./settings";
|
|||||||
import { FlashMessage } from "ui/modules/server/FlashMessage";
|
import { FlashMessage } from "ui/modules/server/FlashMessage";
|
||||||
import { AuthRegister } from "ui/routes/auth/auth.register";
|
import { AuthRegister } from "ui/routes/auth/auth.register";
|
||||||
import { BkndModalsProvider } from "ui/modals";
|
import { BkndModalsProvider } from "ui/modals";
|
||||||
import { useBkndWindowContext } from "ui/client";
|
import { useBkndWindowContext } from "bknd/client";
|
||||||
import ToolsRoutes from "./tools";
|
import ToolsRoutes from "./tools";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { IconPhoto } from "@tabler/icons-react";
|
import { IconPhoto } from "@tabler/icons-react";
|
||||||
import { useBknd } from "ui/client/BkndProvider";
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
import { Empty } from "ui/components/display/Empty";
|
import { Empty } from "ui/components/display/Empty";
|
||||||
import { type FileState, Media } from "ui/elements";
|
|
||||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { bkndModals } from "ui/modals";
|
import { bkndModals } from "ui/modals";
|
||||||
|
import { DropzoneContainer } from "ui/elements/media/DropzoneContainer";
|
||||||
|
import type { FileState } from "ui/elements/media/Dropzone";
|
||||||
|
|
||||||
export function MediaIndex() {
|
export function MediaIndex() {
|
||||||
const { config } = useBknd();
|
const { config } = useBknd();
|
||||||
@@ -35,7 +36,7 @@ export function MediaIndex() {
|
|||||||
return (
|
return (
|
||||||
<AppShell.Scrollable>
|
<AppShell.Scrollable>
|
||||||
<div className="flex flex-1 p-3">
|
<div className="flex flex-1 p-3">
|
||||||
<Media.Dropzone onClick={onClick} infinite query={{ sort: "-id" }} />
|
<DropzoneContainer onClick={onClick} infinite query={{ sort: "-id" }} />
|
||||||
</div>
|
</div>
|
||||||
</AppShell.Scrollable>
|
</AppShell.Scrollable>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IconHome } from "@tabler/icons-react";
|
import { IconHome } from "@tabler/icons-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useAuth } from "ui/client";
|
import { useAuth } from "bknd/client";
|
||||||
import { useEffectOnce } from "ui/hooks/use-effect";
|
import { useEffectOnce } from "ui/hooks/use-effect";
|
||||||
import { Empty } from "../components/display/Empty";
|
import { Empty } from "../components/display/Empty";
|
||||||
import { useBrowserTitle } from "../hooks/use-browser-title";
|
import { useBrowserTitle } from "../hooks/use-browser-title";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useApi, useApiQuery } from "ui/client";
|
import { useApi, useApiQuery } from "bknd/client";
|
||||||
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
||||||
|
|
||||||
function Bla() {
|
function Bla() {
|
||||||
|
|||||||
@@ -35,7 +35,8 @@
|
|||||||
"bknd/adapter": ["./src/adapter/index.ts"],
|
"bknd/adapter": ["./src/adapter/index.ts"],
|
||||||
"bknd/adapter/*": ["./src/adapter/*/index.ts"],
|
"bknd/adapter/*": ["./src/adapter/*/index.ts"],
|
||||||
"bknd/client": ["./src/ui/client/index.ts"],
|
"bknd/client": ["./src/ui/client/index.ts"],
|
||||||
"bknd/modes": ["./src/modes/index.ts"]
|
"bknd/modes": ["./src/modes/index.ts"],
|
||||||
|
"bknd/elements": ["./src/ui/elements/index.ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
"css": {
|
"css": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentWidth": 3
|
"indentWidth": 3
|
||||||
|
},
|
||||||
|
"parser": {
|
||||||
|
"tailwindDirectives": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"json": {
|
"json": {
|
||||||
|
|||||||
117
bun.lock
117
bun.lock
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bknd",
|
"name": "bknd",
|
||||||
@@ -69,6 +70,7 @@
|
|||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
|
"@types/pg": "^8.15.6",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
@@ -80,14 +82,17 @@
|
|||||||
"jotai": "^2.12.2",
|
"jotai": "^2.12.2",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"kysely-generic-sqlite": "^1.2.1",
|
"kysely-generic-sqlite": "^1.2.1",
|
||||||
|
"kysely-postgres-js": "^2.0.0",
|
||||||
"libsql": "^0.5.22",
|
"libsql": "^0.5.22",
|
||||||
"libsql-stateless-easy": "^1.8.0",
|
"libsql-stateless-easy": "^1.8.0",
|
||||||
"miniflare": "^4.20251011.2",
|
"miniflare": "^4.20251011.2",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
"posthog-js-lite": "^3.6.0",
|
"posthog-js-lite": "^3.6.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -146,26 +151,6 @@
|
|||||||
"react-dom": ">=18",
|
"react-dom": ">=18",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/postgres": {
|
|
||||||
"name": "@bknd/postgres",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bun": "^1.2.5",
|
|
||||||
"@types/node": "^22.13.10",
|
|
||||||
"@types/pg": "^8.11.11",
|
|
||||||
"@xata.io/client": "^0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa",
|
|
||||||
"@xata.io/kysely": "^0.2.1",
|
|
||||||
"bknd": "workspace:*",
|
|
||||||
"kysely-neon": "^1.3.0",
|
|
||||||
"tsup": "^8.4.0",
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"kysely": "^0.27.6",
|
|
||||||
"kysely-postgres-js": "^2.0.0",
|
|
||||||
"pg": "^8.14.0",
|
|
||||||
"postgres": "^3.4.7",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/sqlocal": {
|
"packages/sqlocal": {
|
||||||
"name": "@bknd/sqlocal",
|
"name": "@bknd/sqlocal",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
@@ -500,8 +485,6 @@
|
|||||||
|
|
||||||
"@bknd/plasmic": ["@bknd/plasmic@workspace:packages/plasmic"],
|
"@bknd/plasmic": ["@bknd/plasmic@workspace:packages/plasmic"],
|
||||||
|
|
||||||
"@bknd/postgres": ["@bknd/postgres@workspace:packages/postgres"],
|
|
||||||
|
|
||||||
"@bknd/sqlocal": ["@bknd/sqlocal@workspace:packages/sqlocal"],
|
"@bknd/sqlocal": ["@bknd/sqlocal@workspace:packages/sqlocal"],
|
||||||
|
|
||||||
"@bluwy/giget-core": ["@bluwy/giget-core@0.1.6", "", { "dependencies": { "modern-tar": "^0.3.5" } }, "sha512-5BwSIzqhpzXKUnSSheB0M+Qb4iGskepb35FiPA1/7AciPArTqt9H5yc53NmV21gNkDFrgbDBuzSWwrlo2aAKxg=="],
|
"@bluwy/giget-core": ["@bluwy/giget-core@0.1.6", "", { "dependencies": { "modern-tar": "^0.3.5" } }, "sha512-5BwSIzqhpzXKUnSSheB0M+Qb4iGskepb35FiPA1/7AciPArTqt9H5yc53NmV21gNkDFrgbDBuzSWwrlo2aAKxg=="],
|
||||||
@@ -804,8 +787,6 @@
|
|||||||
|
|
||||||
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
|
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
|
||||||
|
|
||||||
"@neondatabase/serverless": ["@neondatabase/serverless@0.4.26", "", { "dependencies": { "@types/pg": "8.6.6" } }, "sha512-6DYEKos2GYn8NTgcJf33BLAx//LcgqzHVavQWe6ZkaDqmEq0I0Xtub6pzwFdq9iayNdCj7e2b0QKr5a8QKB8kQ=="],
|
|
||||||
|
|
||||||
"@next/env": ["@next/env@15.3.5", "", {}, "sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g=="],
|
"@next/env": ["@next/env@15.3.5", "", {}, "sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g=="],
|
||||||
|
|
||||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w=="],
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w=="],
|
||||||
@@ -1312,7 +1293,7 @@
|
|||||||
|
|
||||||
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
|
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
|
||||||
|
|
||||||
"@types/pg": ["@types/pg@8.11.11", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw=="],
|
"@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
|
||||||
|
|
||||||
"@types/prettier": ["@types/prettier@1.19.1", "", {}, "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ=="],
|
"@types/prettier": ["@types/prettier@1.19.1", "", {}, "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ=="],
|
||||||
|
|
||||||
@@ -1430,10 +1411,6 @@
|
|||||||
|
|
||||||
"@wdio/utils": ["@wdio/utils@9.11.0", "", { "dependencies": { "@puppeteer/browsers": "^2.2.0", "@wdio/logger": "9.4.4", "@wdio/types": "9.10.1", "decamelize": "^6.0.0", "deepmerge-ts": "^7.0.3", "edgedriver": "^6.1.1", "geckodriver": "^5.0.0", "get-port": "^7.0.0", "import-meta-resolve": "^4.0.0", "locate-app": "^2.2.24", "safaridriver": "^1.0.0", "split2": "^4.2.0", "wait-port": "^1.1.0" } }, "sha512-chVbHqrjDlIKCLoAPLdrFK8Qozu/S+fbubqlyazohAKnouCUCa2goYs7faYR0lkmLqm92PllJS+KBRAha9V/tg=="],
|
"@wdio/utils": ["@wdio/utils@9.11.0", "", { "dependencies": { "@puppeteer/browsers": "^2.2.0", "@wdio/logger": "9.4.4", "@wdio/types": "9.10.1", "decamelize": "^6.0.0", "deepmerge-ts": "^7.0.3", "edgedriver": "^6.1.1", "geckodriver": "^5.0.0", "get-port": "^7.0.0", "import-meta-resolve": "^4.0.0", "locate-app": "^2.2.24", "safaridriver": "^1.0.0", "split2": "^4.2.0", "wait-port": "^1.1.0" } }, "sha512-chVbHqrjDlIKCLoAPLdrFK8Qozu/S+fbubqlyazohAKnouCUCa2goYs7faYR0lkmLqm92PllJS+KBRAha9V/tg=="],
|
||||||
|
|
||||||
"@xata.io/client": ["@xata.io/client@0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa", "", { "peerDependencies": { "typescript": ">=4.5" } }, "sha512-4Js4SAKwmmOPmZVIS1l2K8XVGGkUOi8L1jXuagDfeUX56n95wfA4xYMSmsVS0RLMmRWI4UM4bp5UcFJxwbFYGw=="],
|
|
||||||
|
|
||||||
"@xata.io/kysely": ["@xata.io/kysely@0.2.1", "", { "dependencies": { "@xata.io/client": "0.30.1" }, "peerDependencies": { "kysely": "*" } }, "sha512-0+WBcFkBSNEu11wVTyJyeNMOPUuolDKJMjXQr1nheHTNZLfsL0qKshTZOKIC/bGInjepGA7DQ/HFeKDHe5CDpA=="],
|
|
||||||
|
|
||||||
"@xyflow/react": ["@xyflow/react@12.9.2", "", { "dependencies": { "@xyflow/system": "0.0.72", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-Xr+LFcysHCCoc5KRHaw+FwbqbWYxp9tWtk1mshNcqy25OAPuaKzXSdqIMNOA82TIXF/gFKo0Wgpa6PU7wUUVqw=="],
|
"@xyflow/react": ["@xyflow/react@12.9.2", "", { "dependencies": { "@xyflow/system": "0.0.72", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-Xr+LFcysHCCoc5KRHaw+FwbqbWYxp9tWtk1mshNcqy25OAPuaKzXSdqIMNOA82TIXF/gFKo0Wgpa6PU7wUUVqw=="],
|
||||||
|
|
||||||
"@xyflow/system": ["@xyflow/system@0.0.72", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-WBI5Aau0fXTXwxHPzceLNS6QdXggSWnGjDtj/gG669crApN8+SCmEtkBth1m7r6pStNo/5fI9McEi7Dk0ymCLA=="],
|
"@xyflow/system": ["@xyflow/system@0.0.72", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-WBI5Aau0fXTXwxHPzceLNS6QdXggSWnGjDtj/gG669crApN8+SCmEtkBth1m7r6pStNo/5fI9McEi7Dk0ymCLA=="],
|
||||||
@@ -2578,8 +2555,6 @@
|
|||||||
|
|
||||||
"kysely-generic-sqlite": ["kysely-generic-sqlite@1.2.1", "", { "peerDependencies": { "kysely": ">=0.26" } }, "sha512-/Bs3/Uktn04nQ9g/4oSphLMEtSHkS5+j5hbKjK5gMqXQfqr/v3V3FKtoN4pLTmo2W35hNdrIpQnBukGL1zZc6g=="],
|
"kysely-generic-sqlite": ["kysely-generic-sqlite@1.2.1", "", { "peerDependencies": { "kysely": ">=0.26" } }, "sha512-/Bs3/Uktn04nQ9g/4oSphLMEtSHkS5+j5hbKjK5gMqXQfqr/v3V3FKtoN4pLTmo2W35hNdrIpQnBukGL1zZc6g=="],
|
||||||
|
|
||||||
"kysely-neon": ["kysely-neon@1.3.0", "", { "peerDependencies": { "@neondatabase/serverless": "^0.4.3", "kysely": "0.x.x", "ws": "^8.13.0" }, "optionalPeers": ["ws"] }, "sha512-CIIlbmqpIXVJDdBEYtEOwbmALag0jmqYrGfBeM4cHKb9AgBGs+X1SvXUZ8TqkDacQEqEZN2XtsDoUkcMIISjHw=="],
|
|
||||||
|
|
||||||
"kysely-postgres-js": ["kysely-postgres-js@2.0.0", "", { "peerDependencies": { "kysely": ">= 0.24.0 < 1", "postgres": ">= 3.4.0 < 4" } }, "sha512-R1tWx6/x3tSatWvsmbHJxpBZYhNNxcnMw52QzZaHKg7ZOWtHib4iZyEaw4gb2hNKVctWQ3jfMxZT/ZaEMK6kBQ=="],
|
"kysely-postgres-js": ["kysely-postgres-js@2.0.0", "", { "peerDependencies": { "kysely": ">= 0.24.0 < 1", "postgres": ">= 3.4.0 < 4" } }, "sha512-R1tWx6/x3tSatWvsmbHJxpBZYhNNxcnMw52QzZaHKg7ZOWtHib4iZyEaw4gb2hNKVctWQ3jfMxZT/ZaEMK6kBQ=="],
|
||||||
|
|
||||||
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
||||||
@@ -2844,8 +2819,6 @@
|
|||||||
|
|
||||||
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
|
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
|
||||||
|
|
||||||
"obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="],
|
|
||||||
|
|
||||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||||
|
|
||||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
@@ -2930,21 +2903,19 @@
|
|||||||
|
|
||||||
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
|
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
|
||||||
|
|
||||||
"pg": ["pg@8.14.0", "", { "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.8.0", "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-nXbVpyoaXVmdqlKEzToFf37qzyeeh7mbiXsnoWvstSqohj88yaa/I/Rq/HEVn2QPSZEuLIJa/jSpRDyzjEx4FQ=="],
|
"pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
|
||||||
|
|
||||||
"pg-cloudflare": ["pg-cloudflare@1.1.1", "", {}, "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="],
|
"pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
|
||||||
|
|
||||||
"pg-connection-string": ["pg-connection-string@2.7.0", "", {}, "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="],
|
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
|
||||||
|
|
||||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||||
|
|
||||||
"pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="],
|
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
|
||||||
|
|
||||||
"pg-pool": ["pg-pool@3.8.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw=="],
|
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
|
||||||
|
|
||||||
"pg-protocol": ["pg-protocol@1.8.0", "", {}, "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g=="],
|
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||||
|
|
||||||
"pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
|
|
||||||
|
|
||||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
@@ -2998,15 +2969,13 @@
|
|||||||
|
|
||||||
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
|
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
|
||||||
|
|
||||||
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
|
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
||||||
|
|
||||||
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
|
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||||
|
|
||||||
"postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
|
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||||
|
|
||||||
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
|
|
||||||
|
|
||||||
"posthog-js-lite": ["posthog-js-lite@3.6.0", "", {}, "sha512-4NrnGwBna7UZ0KARVdHE7Udm/os9HoYRJDHWC55xj1UebBkFRDM+fIxCRovVCmEtuF27oNoDH+pTc81iWAyK7g=="],
|
"posthog-js-lite": ["posthog-js-lite@3.6.0", "", {}, "sha512-4NrnGwBna7UZ0KARVdHE7Udm/os9HoYRJDHWC55xj1UebBkFRDM+fIxCRovVCmEtuF27oNoDH+pTc81iWAyK7g=="],
|
||||||
|
|
||||||
@@ -3484,7 +3453,7 @@
|
|||||||
|
|
||||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
"tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
|
||||||
|
|
||||||
"tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="],
|
"tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="],
|
||||||
|
|
||||||
@@ -3896,7 +3865,7 @@
|
|||||||
|
|
||||||
"@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
"@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||||
|
|
||||||
"@bknd/plasmic/@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
"@bknd/plasmic/@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
||||||
|
|
||||||
"@bundled-es-modules/tough-cookie/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="],
|
"@bundled-es-modules/tough-cookie/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="],
|
||||||
|
|
||||||
@@ -3956,8 +3925,6 @@
|
|||||||
|
|
||||||
"@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
"@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||||
|
|
||||||
"@neondatabase/serverless/@types/pg": ["@types/pg@8.6.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw=="],
|
|
||||||
|
|
||||||
"@plasmicapp/nextjs-app-router/cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"@plasmicapp/nextjs-app-router/cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
"@plasmicapp/query/swr": ["swr@1.3.0", "", { "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0" } }, "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw=="],
|
"@plasmicapp/query/swr": ["swr@1.3.0", "", { "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0" } }, "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw=="],
|
||||||
@@ -4128,7 +4095,7 @@
|
|||||||
|
|
||||||
"@types/graceful-fs/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
"@types/graceful-fs/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
||||||
|
|
||||||
"@types/pg/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
"@types/pg/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
||||||
|
|
||||||
"@types/resolve/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
"@types/resolve/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
||||||
|
|
||||||
@@ -4178,8 +4145,6 @@
|
|||||||
|
|
||||||
"@vitest/mocker/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
"@vitest/mocker/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||||
|
|
||||||
"@vitest/ui/tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
|
|
||||||
|
|
||||||
"@vitest/ui/vitest": ["vitest@3.0.8", "", { "dependencies": { "@vitest/expect": "3.0.8", "@vitest/mocker": "3.0.8", "@vitest/pretty-format": "^3.0.8", "@vitest/runner": "3.0.8", "@vitest/snapshot": "3.0.8", "@vitest/spy": "3.0.8", "@vitest/utils": "3.0.8", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.8", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.8", "@vitest/ui": "3.0.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA=="],
|
"@vitest/ui/vitest": ["vitest@3.0.8", "", { "dependencies": { "@vitest/expect": "3.0.8", "@vitest/mocker": "3.0.8", "@vitest/pretty-format": "^3.0.8", "@vitest/runner": "3.0.8", "@vitest/snapshot": "3.0.8", "@vitest/spy": "3.0.8", "@vitest/utils": "3.0.8", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.8", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.8", "@vitest/ui": "3.0.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA=="],
|
||||||
|
|
||||||
"@wdio/config/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
"@wdio/config/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||||
@@ -4192,8 +4157,6 @@
|
|||||||
|
|
||||||
"@wdio/types/@types/node": ["@types/node@20.17.24", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA=="],
|
"@wdio/types/@types/node": ["@types/node@20.17.24", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA=="],
|
||||||
|
|
||||||
"@xata.io/kysely/@xata.io/client": ["@xata.io/client@0.30.1", "", { "peerDependencies": { "typescript": ">=4.5" } }, "sha512-dAzDPHmIfenVIpF39m1elmW5ngjWu2mO8ZqJBN7dmYdXr98uhPANfLdVZnc3mUNG+NH37LqY1dSO862hIo2oRw=="],
|
|
||||||
|
|
||||||
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
"acorn-globals/acorn": ["acorn@6.4.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="],
|
"acorn-globals/acorn": ["acorn@6.4.2", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="],
|
||||||
@@ -4526,8 +4489,6 @@
|
|||||||
|
|
||||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
|
||||||
|
|
||||||
"pino/process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
"pino/process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
||||||
|
|
||||||
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
@@ -4536,6 +4497,8 @@
|
|||||||
|
|
||||||
"postcss-mixins/sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="],
|
"postcss-mixins/sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="],
|
||||||
|
|
||||||
|
"postcss-mixins/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"pretty-format/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"pretty-format/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
"progress-estimator/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
"progress-estimator/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||||
@@ -4684,7 +4647,7 @@
|
|||||||
|
|
||||||
"through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
"tinyglobby/fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
"tinyglobby/fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
||||||
|
|
||||||
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||||
|
|
||||||
@@ -4706,6 +4669,8 @@
|
|||||||
|
|
||||||
"tsup/rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],
|
"tsup/rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],
|
||||||
|
|
||||||
|
"tsup/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"union-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
"union-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||||
|
|
||||||
"unset-value/has-value": ["has-value@0.3.1", "", { "dependencies": { "get-value": "^2.0.3", "has-values": "^0.1.4", "isobject": "^2.0.0" } }, "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q=="],
|
"unset-value/has-value": ["has-value@0.3.1", "", { "dependencies": { "get-value": "^2.0.3", "has-values": "^0.1.4", "isobject": "^2.0.0" } }, "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q=="],
|
||||||
@@ -4794,7 +4759,7 @@
|
|||||||
|
|
||||||
"@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="],
|
"@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="],
|
||||||
|
|
||||||
"@bknd/plasmic/@types/bun/bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
"@bknd/plasmic/@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||||
|
|
||||||
"@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="],
|
"@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="],
|
||||||
|
|
||||||
@@ -4814,10 +4779,6 @@
|
|||||||
|
|
||||||
"@jridgewell/remapping/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
"@jridgewell/remapping/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
"@neondatabase/serverless/@types/pg/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
|
||||||
|
|
||||||
"@neondatabase/serverless/@types/pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
|
||||||
|
|
||||||
"@plasmicapp/nextjs-app-router/cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"@plasmicapp/nextjs-app-router/cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
"@plasmicapp/nextjs-app-router/cross-spawn/shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"@plasmicapp/nextjs-app-router/cross-spawn/shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
@@ -4882,7 +4843,7 @@
|
|||||||
|
|
||||||
"@types/graceful-fs/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
"@types/graceful-fs/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||||
|
|
||||||
"@types/pg/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
"@types/pg/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
"@types/resolve/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
"@types/resolve/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||||
|
|
||||||
@@ -4924,9 +4885,7 @@
|
|||||||
|
|
||||||
"@vitest/mocker/vite/rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],
|
"@vitest/mocker/vite/rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],
|
||||||
|
|
||||||
"@vitest/ui/tinyglobby/fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
"@vitest/mocker/vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"@vitest/ui/tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
|
||||||
|
|
||||||
"@vitest/ui/vitest/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
"@vitest/ui/vitest/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||||
|
|
||||||
@@ -5140,13 +5099,9 @@
|
|||||||
|
|
||||||
"object-copy/define-property/is-descriptor": ["is-descriptor@0.1.7", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg=="],
|
"object-copy/define-property/is-descriptor": ["is-descriptor@0.1.7", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg=="],
|
||||||
|
|
||||||
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
"postcss-mixins/tinyglobby/fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
||||||
|
|
||||||
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
"postcss-mixins/tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||||
|
|
||||||
"pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
|
||||||
|
|
||||||
"pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
|
||||||
|
|
||||||
"progress-estimator/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
"progress-estimator/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||||
|
|
||||||
@@ -5212,6 +5167,10 @@
|
|||||||
|
|
||||||
"tsc-alias/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"tsc-alias/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"tsup/tinyglobby/fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
||||||
|
|
||||||
|
"tsup/tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||||
|
|
||||||
"unset-value/has-value/has-values": ["has-values@0.1.4", "", {}, "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ=="],
|
"unset-value/has-value/has-values": ["has-values@0.1.4", "", {}, "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ=="],
|
||||||
|
|
||||||
"unset-value/has-value/isobject": ["isobject@2.1.0", "", { "dependencies": { "isarray": "1.0.0" } }, "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA=="],
|
"unset-value/has-value/isobject": ["isobject@2.1.0", "", { "dependencies": { "isarray": "1.0.0" } }, "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA=="],
|
||||||
@@ -5312,16 +5271,6 @@
|
|||||||
|
|
||||||
"@cloudflare/vitest-pool-workers/miniflare/youch/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
"@cloudflare/vitest-pool-workers/miniflare/youch/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||||
|
|
||||||
"@neondatabase/serverless/@types/pg/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
|
||||||
|
|
||||||
"@neondatabase/serverless/@types/pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
|
||||||
|
|
||||||
"@neondatabase/serverless/@types/pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
|
||||||
|
|
||||||
"@neondatabase/serverless/@types/pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
|
||||||
|
|
||||||
"@neondatabase/serverless/@types/pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
|
||||||
|
|
||||||
"@plasmicapp/nextjs-app-router/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
"@plasmicapp/nextjs-app-router/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
"@vitejs/plugin-react/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
[install]
|
[install]
|
||||||
#linker = "hoisted"
|
linker = "isolated"
|
||||||
@@ -261,3 +261,77 @@ export default {
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### `emailOTP`
|
||||||
|
|
||||||
|
<Callout type="warning">
|
||||||
|
Make sure to setup proper permissions to restrict reading from the OTP entity. Also, this plugin requires the `email` driver to be registered.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
|
||||||
|
A plugin that adds email OTP functionality to your app. It will add two endpoints to your app:
|
||||||
|
- `POST /api/auth/otp/login` to login a user with an OTP code
|
||||||
|
- `POST /api/auth/otp/register` to register a user with an OTP code
|
||||||
|
|
||||||
|
Both endpoints accept a JSON body with `email` (required) and `code` (optional). If `code` is provided, the OTP code will be validated and the user will be logged in or registered. If `code` is not provided, a new OTP code will be generated and sent to the user's email.
|
||||||
|
|
||||||
|
For example, to login an existing user with an OTP code, two requests are needed. The first one only with the email to generate and send the OTP code, and the second to send the users' email along with the OTP code. The last request will authenticate the user.
|
||||||
|
|
||||||
|
```http title="Generate OTP code to login"
|
||||||
|
POST /api/auth/otp/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "test@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the user exists, an email will be sent with the OTP code, and the response will be a `201 Created`.
|
||||||
|
|
||||||
|
```http title="Login with OTP code"
|
||||||
|
POST /api/auth/otp/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "test@example.com",
|
||||||
|
"code": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the code is valid, the user will be authenticated by sending a `Set-Cookie` header and a body property `token` with the JWT token (equally to the login endpoint).
|
||||||
|
|
||||||
|
|
||||||
|
```typescript title="bknd.config.ts"
|
||||||
|
import { emailOTP } from "bknd/plugins";
|
||||||
|
import { resendEmail } from "bknd";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
options: {
|
||||||
|
drivers: {
|
||||||
|
// an email driver is required
|
||||||
|
email: resendEmail({ /* ... */}),
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// all options are optional
|
||||||
|
emailOTP({
|
||||||
|
// the base path for the API endpoints
|
||||||
|
apiBasePath: "/api/auth/otp",
|
||||||
|
// the TTL for the OTP tokens in seconds
|
||||||
|
ttl: 600,
|
||||||
|
// the name of the OTP entity
|
||||||
|
entity: "users_otp",
|
||||||
|
// customize the email content
|
||||||
|
generateEmail: (otp) => ({
|
||||||
|
subject: "OTP Code",
|
||||||
|
body: `Your OTP code is: ${otp.code}`,
|
||||||
|
}),
|
||||||
|
// customize the code generation
|
||||||
|
generateCode: (user) => {
|
||||||
|
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} satisfies BkndConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
<AutoTypeTable path="../app/src/plugins/auth/email-otp.plugin.ts" name="EmailOTPPluginOptions" />
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ npm install @bknd/sqlocal
|
|||||||
|
|
||||||
This package uses `sqlocal` under the hood. Consult the [sqlocal documentation](https://sqlocal.dallashoffman.com/guide/setup) for connection options:
|
This package uses `sqlocal` under the hood. Consult the [sqlocal documentation](https://sqlocal.dallashoffman.com/guide/setup) for connection options:
|
||||||
|
|
||||||
```js
|
```ts
|
||||||
import { createApp } from "bknd";
|
import { createApp } from "bknd";
|
||||||
import { SQLocalConnection } from "@bknd/sqlocal";
|
import { SQLocalConnection } from "@bknd/sqlocal";
|
||||||
|
|
||||||
@@ -210,42 +210,39 @@ const app = createApp({
|
|||||||
|
|
||||||
## PostgreSQL
|
## PostgreSQL
|
||||||
|
|
||||||
To use bknd with Postgres, you need to install the `@bknd/postgres` package. You can do so by running the following command:
|
Postgres is built-in to bknd, you can connect to your Postgres database using `pg` or `postgres` dialects. Additionally, you may also define your custom connection.
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @bknd/postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
You can connect to your Postgres database using `pg` or `postgres` dialects. Additionally, you may also define your custom connection.
|
|
||||||
|
|
||||||
### Using `pg`
|
### Using `pg`
|
||||||
|
|
||||||
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package.
|
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package. Wrap the `Pool` in the `pg` function to create a connection.
|
||||||
|
|
||||||
```js
|
```ts
|
||||||
import { serve } from "bknd/adapter/node";
|
import { serve } from "bknd/adapter/node";
|
||||||
import { pg } from "@bknd/postgres";
|
import { pg } from "bknd";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
/** @type {import("bknd/adapter/node").NodeBkndConfig} */
|
serve({
|
||||||
const config = {
|
|
||||||
connection: pg({
|
connection: pg({
|
||||||
connectionString: "postgresql://user:password@localhost:5432/database",
|
pool: new Pool({
|
||||||
|
connectionString: "postgresql://user:password@localhost:5432/database",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
});
|
||||||
|
|
||||||
serve(config);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using `postgres`
|
### Using `postgres`
|
||||||
|
|
||||||
To establish a connection to your database, you can use any connection options available on the [`postgres`](https://github.com/porsager/postgres) package.
|
To establish a connection to your database, you can use any connection options available on the [`postgres`](https://github.com/porsager/postgres) package. Wrap the `Sql` in the `postgresJs` function to create a connection.
|
||||||
|
|
||||||
```js
|
```ts
|
||||||
import { serve } from "bknd/adapter/node";
|
import { serve } from "bknd/adapter/node";
|
||||||
import { postgresJs } from "@bknd/postgres";
|
import { postgresJs } from "bknd";
|
||||||
|
import postgres from 'postgres'
|
||||||
|
|
||||||
serve({
|
serve({
|
||||||
connection: postgresJs("postgresql://user:password@localhost:5432/database"),
|
connection: postgresJs({
|
||||||
|
postgres: postgres("postgresql://user:password@localhost:5432/database"),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -255,8 +252,8 @@ Several Postgres hosting providers offer their own clients to connect to their d
|
|||||||
|
|
||||||
Example using `@neondatabase/serverless`:
|
Example using `@neondatabase/serverless`:
|
||||||
|
|
||||||
```js
|
```ts
|
||||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
import { createCustomPostgresConnection } from "bknd";
|
||||||
import { NeonDialect } from "kysely-neon";
|
import { NeonDialect } from "kysely-neon";
|
||||||
|
|
||||||
const neon = createCustomPostgresConnection("neon", NeonDialect);
|
const neon = createCustomPostgresConnection("neon", NeonDialect);
|
||||||
@@ -270,8 +267,8 @@ serve({
|
|||||||
|
|
||||||
Example using `@xata.io/client`:
|
Example using `@xata.io/client`:
|
||||||
|
|
||||||
```js
|
```ts
|
||||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
import { createCustomPostgresConnection } from "bknd";
|
||||||
import { XataDialect } from "@xata.io/kysely";
|
import { XataDialect } from "@xata.io/kysely";
|
||||||
import { buildClient } from "@xata.io/client";
|
import { buildClient } from "@xata.io/client";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { serve } from "bknd/adapter/bun";
|
import { serve } from "bknd/adapter/bun";
|
||||||
import { createCustomPostgresConnection } from "../src";
|
import { createCustomPostgresConnection } from "bknd";
|
||||||
import { NeonDialect } from "kysely-neon";
|
import { NeonDialect } from "kysely-neon";
|
||||||
|
|
||||||
const neon = createCustomPostgresConnection(NeonDialect);
|
const neon = createCustomPostgresConnection("neon", NeonDialect);
|
||||||
|
|
||||||
export default serve({
|
export default serve({
|
||||||
connection: neon({
|
connection: neon({
|
||||||
20
examples/postgres/package.json
Normal file
20
examples/postgres/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "postgres",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"pg": "^8.14.0",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
|
"@xata.io/client": "^0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa",
|
||||||
|
"@xata.io/kysely": "^0.2.1",
|
||||||
|
"kysely-neon": "^1.3.0",
|
||||||
|
"bknd": "file:../app",
|
||||||
|
"kysely": "0.27.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.2.5",
|
||||||
|
"@types/node": "^22.13.10",
|
||||||
|
"@types/pg": "^8.11.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"$bknd/*": ["../../app/src/*"]
|
"bknd": ["../app/src/index.ts"],
|
||||||
|
"bknd/*": ["../app/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts"],
|
"include": ["./src/**/*.ts"],
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import { serve } from "bknd/adapter/bun";
|
import { serve } from "bknd/adapter/bun";
|
||||||
import { createCustomPostgresConnection } from "../src";
|
import { createCustomPostgresConnection } from "bknd";
|
||||||
import { XataDialect } from "@xata.io/kysely";
|
import { XataDialect } from "@xata.io/kysely";
|
||||||
import { buildClient } from "@xata.io/client";
|
import { buildClient } from "@xata.io/client";
|
||||||
|
|
||||||
const client = buildClient();
|
const client = buildClient();
|
||||||
const xata = new client({
|
const xataClient = new client({
|
||||||
databaseURL: process.env.XATA_URL,
|
databaseURL: process.env.XATA_URL,
|
||||||
apiKey: process.env.XATA_API_KEY,
|
apiKey: process.env.XATA_API_KEY,
|
||||||
branch: process.env.XATA_BRANCH,
|
branch: process.env.XATA_BRANCH,
|
||||||
});
|
});
|
||||||
|
|
||||||
const connection = createCustomPostgresConnection(XataDialect, {
|
const xata = createCustomPostgresConnection("xata", XataDialect, {
|
||||||
supports: {
|
supports: {
|
||||||
batching: false,
|
batching: false,
|
||||||
},
|
},
|
||||||
})({ xata });
|
});
|
||||||
|
|
||||||
export default serve({
|
export default serve({
|
||||||
connection,
|
connection: xata(xataClient),
|
||||||
// ignore this, it's only required within this repository
|
// ignore this, it's only required within this repository
|
||||||
// because bknd is installed via "workspace:*"
|
// because bknd is installed via "workspace:*"
|
||||||
distPath: "../../../app/dist",
|
distPath: "../../../app/dist",
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.2",
|
"packageManager": "bun@1.3.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.13"
|
"node": ">=22.13"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
# Postgres adapter for `bknd` (experimental)
|
|
||||||
This packages adds an adapter to use a Postgres database with [`bknd`](https://github.com/bknd-io/bknd). It works with both `pg` and `postgres` drivers, and supports custom postgres connections.
|
|
||||||
* works with any Postgres database (tested with Supabase, Neon, Xata, and RDS)
|
|
||||||
* choose between `pg` and `postgres` drivers
|
|
||||||
* create custom postgres connections with any kysely postgres dialect
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Install the adapter with:
|
|
||||||
```bash
|
|
||||||
npm install @bknd/postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using `pg` driver
|
|
||||||
Install the [`pg`](https://github.com/brianc/node-postgres) driver with:
|
|
||||||
```bash
|
|
||||||
npm install pg
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a connection:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { pg } from "@bknd/postgres";
|
|
||||||
|
|
||||||
// accepts `pg` configuration
|
|
||||||
const connection = pg({
|
|
||||||
host: "localhost",
|
|
||||||
port: 5432,
|
|
||||||
user: "postgres",
|
|
||||||
password: "postgres",
|
|
||||||
database: "postgres",
|
|
||||||
});
|
|
||||||
|
|
||||||
// or with a connection string
|
|
||||||
const connection = pg({
|
|
||||||
connectionString: "postgres://postgres:postgres@localhost:5432/postgres",
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using `postgres` driver
|
|
||||||
|
|
||||||
Install the [`postgres`](https://github.com/porsager/postgres) driver with:
|
|
||||||
```bash
|
|
||||||
npm install postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a connection:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { postgresJs } from "@bknd/postgres";
|
|
||||||
|
|
||||||
// accepts `postgres` configuration
|
|
||||||
const connection = postgresJs("postgres://postgres:postgres@localhost:5432/postgres");
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using custom postgres dialects
|
|
||||||
|
|
||||||
You can create a custom kysely postgres dialect by using the `createCustomPostgresConnection` function.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
|
||||||
|
|
||||||
const connection = createCustomPostgresConnection("my_postgres_dialect", MyDialect)({
|
|
||||||
// your custom dialect configuration
|
|
||||||
supports: {
|
|
||||||
batching: true
|
|
||||||
},
|
|
||||||
excludeTables: ["my_table"],
|
|
||||||
plugins: [new MyKyselyPlugin()],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom `neon` connection
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
|
||||||
import { NeonDialect } from "kysely-neon";
|
|
||||||
|
|
||||||
const connection = createCustomPostgresConnection("neon", NeonDialect)({
|
|
||||||
connectionString: process.env.NEON,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom `xata` connection
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
|
||||||
import { XataDialect } from "@xata.io/kysely";
|
|
||||||
import { buildClient } from "@xata.io/client";
|
|
||||||
|
|
||||||
const client = buildClient();
|
|
||||||
const xata = new client({
|
|
||||||
databaseURL: process.env.XATA_URL,
|
|
||||||
apiKey: process.env.XATA_API_KEY,
|
|
||||||
branch: process.env.XATA_BRANCH,
|
|
||||||
});
|
|
||||||
|
|
||||||
const connection = createCustomPostgresConnection("xata", XataDialect, {
|
|
||||||
supports: {
|
|
||||||
batching: false,
|
|
||||||
},
|
|
||||||
})({ xata });
|
|
||||||
```
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@bknd/postgres",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.js",
|
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsup",
|
|
||||||
"test": "bun test",
|
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"updater": "bun x npm-check-updates -ui",
|
|
||||||
"prepublishOnly": "bun run typecheck && bun run test && bun run build",
|
|
||||||
"docker:start": "docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_DB=bknd -p 5430:5432 postgres:17",
|
|
||||||
"docker:stop": "docker stop bknd-test-postgres"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"kysely": "^0.27.6",
|
|
||||||
"kysely-postgres-js": "^2.0.0",
|
|
||||||
"pg": "^8.14.0",
|
|
||||||
"postgres": "^3.4.7"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bun": "^1.2.5",
|
|
||||||
"@types/node": "^22.13.10",
|
|
||||||
"@types/pg": "^8.11.11",
|
|
||||||
"@xata.io/client": "^0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa",
|
|
||||||
"@xata.io/kysely": "^0.2.1",
|
|
||||||
"bknd": "workspace:*",
|
|
||||||
"kysely-neon": "^1.3.0",
|
|
||||||
"tsup": "^8.4.0"
|
|
||||||
},
|
|
||||||
"tsup": {
|
|
||||||
"entry": ["src/index.ts"],
|
|
||||||
"format": ["esm"],
|
|
||||||
"target": "es2022",
|
|
||||||
"metafile": true,
|
|
||||||
"clean": true,
|
|
||||||
"minify": true,
|
|
||||||
"dts": true,
|
|
||||||
"external": ["bknd", "pg", "postgres", "kysely", "kysely-postgres-js"]
|
|
||||||
},
|
|
||||||
"files": ["dist", "README.md", "!*.map", "!metafile*.json"]
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Kysely, PostgresDialect } from "kysely";
|
|
||||||
import { PostgresIntrospector } from "./PostgresIntrospector";
|
|
||||||
import { PostgresConnection, plugins } from "./PostgresConnection";
|
|
||||||
import { customIntrospector } from "bknd";
|
|
||||||
import $pg from "pg";
|
|
||||||
|
|
||||||
export type PgPostgresConnectionConfig = $pg.PoolConfig;
|
|
||||||
|
|
||||||
export class PgPostgresConnection extends PostgresConnection {
|
|
||||||
override name = "pg";
|
|
||||||
private pool: $pg.Pool;
|
|
||||||
|
|
||||||
constructor(config: PgPostgresConnectionConfig) {
|
|
||||||
const pool = new $pg.Pool(config);
|
|
||||||
const kysely = new Kysely({
|
|
||||||
dialect: customIntrospector(PostgresDialect, PostgresIntrospector, {
|
|
||||||
excludeTables: [],
|
|
||||||
}).create({ pool }),
|
|
||||||
plugins,
|
|
||||||
});
|
|
||||||
|
|
||||||
super(kysely);
|
|
||||||
this.pool = pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
override async close(): Promise<void> {
|
|
||||||
await this.pool.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pg(config: PgPostgresConnectionConfig): PgPostgresConnection {
|
|
||||||
return new PgPostgresConnection(config);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { Kysely } from "kysely";
|
|
||||||
import { PostgresIntrospector } from "./PostgresIntrospector";
|
|
||||||
import { PostgresConnection, plugins } from "./PostgresConnection";
|
|
||||||
import { customIntrospector } from "bknd";
|
|
||||||
import { PostgresJSDialect } from "kysely-postgres-js";
|
|
||||||
import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres";
|
|
||||||
|
|
||||||
export type PostgresJsConfig = Options<Record<string, PostgresType>>;
|
|
||||||
|
|
||||||
export class PostgresJsConnection extends PostgresConnection {
|
|
||||||
override name = "postgres-js";
|
|
||||||
|
|
||||||
private postgres: Sql;
|
|
||||||
|
|
||||||
constructor(opts: { postgres: Sql }) {
|
|
||||||
const kysely = new Kysely({
|
|
||||||
dialect: customIntrospector(PostgresJSDialect, PostgresIntrospector, {
|
|
||||||
excludeTables: [],
|
|
||||||
}).create({ postgres: opts.postgres }),
|
|
||||||
plugins,
|
|
||||||
});
|
|
||||||
|
|
||||||
super(kysely);
|
|
||||||
this.postgres = opts.postgres;
|
|
||||||
}
|
|
||||||
|
|
||||||
override async close(): Promise<void> {
|
|
||||||
await this.postgres.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function postgresJs(
|
|
||||||
connectionString: string,
|
|
||||||
config?: PostgresJsConfig,
|
|
||||||
): PostgresJsConnection;
|
|
||||||
export function postgresJs(config: PostgresJsConfig): PostgresJsConnection;
|
|
||||||
export function postgresJs(
|
|
||||||
first: PostgresJsConfig | string,
|
|
||||||
second?: PostgresJsConfig,
|
|
||||||
): PostgresJsConnection {
|
|
||||||
const postgres = typeof first === "string" ? $postgresJs(first, second) : $postgresJs(first);
|
|
||||||
return new PostgresJsConnection({ postgres });
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { describe } from "bun:test";
|
|
||||||
import { pg } from "../src/PgPostgresConnection";
|
|
||||||
import { testSuite } from "./suite";
|
|
||||||
|
|
||||||
describe("pg", () => {
|
|
||||||
testSuite({
|
|
||||||
createConnection: () =>
|
|
||||||
pg({
|
|
||||||
host: "localhost",
|
|
||||||
port: 5430,
|
|
||||||
user: "postgres",
|
|
||||||
password: "postgres",
|
|
||||||
database: "bknd",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { describe } from "bun:test";
|
|
||||||
import { postgresJs } from "../src/PostgresJsConnection";
|
|
||||||
import { testSuite } from "./suite";
|
|
||||||
|
|
||||||
describe("postgresjs", () => {
|
|
||||||
testSuite({
|
|
||||||
createConnection: () =>
|
|
||||||
postgresJs({
|
|
||||||
host: "localhost",
|
|
||||||
port: 5430,
|
|
||||||
user: "postgres",
|
|
||||||
password: "postgres",
|
|
||||||
database: "bknd",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test";
|
|
||||||
import type { PostgresConnection } from "../src";
|
|
||||||
import { createApp, em, entity, text } from "bknd";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "bknd/utils";
|
|
||||||
// @ts-ignore
|
|
||||||
import { connectionTestSuite } from "$bknd/data/connection/connection-test-suite";
|
|
||||||
// @ts-ignore
|
|
||||||
import { bunTestRunner } from "$bknd/adapter/bun/test";
|
|
||||||
|
|
||||||
export type TestSuiteConfig = {
|
|
||||||
createConnection: () => InstanceType<typeof PostgresConnection>;
|
|
||||||
cleanDatabase?: (connection: InstanceType<typeof PostgresConnection>) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function defaultCleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
|
|
||||||
const kysely = connection.kysely;
|
|
||||||
|
|
||||||
// drop all tables+indexes & create new schema
|
|
||||||
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
|
|
||||||
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
|
|
||||||
await kysely.schema.createSchema("public").execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanDatabase(
|
|
||||||
connection: InstanceType<typeof PostgresConnection>,
|
|
||||||
config: TestSuiteConfig,
|
|
||||||
) {
|
|
||||||
if (config.cleanDatabase) {
|
|
||||||
await config.cleanDatabase(connection);
|
|
||||||
} else {
|
|
||||||
await defaultCleanDatabase(connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function testSuite(config: TestSuiteConfig) {
|
|
||||||
beforeAll(() => disableConsoleLog(["log", "warn", "error"]));
|
|
||||||
afterAll(() => enableConsoleLog());
|
|
||||||
|
|
||||||
// @todo: postgres seems to add multiple indexes, thus failing the test suite
|
|
||||||
/* describe("test suite", () => {
|
|
||||||
connectionTestSuite(bunTestRunner, {
|
|
||||||
makeConnection: () => {
|
|
||||||
const connection = config.createConnection();
|
|
||||||
return {
|
|
||||||
connection,
|
|
||||||
dispose: async () => {
|
|
||||||
await cleanDatabase(connection, config);
|
|
||||||
await connection.close();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
rawDialectDetails: [],
|
|
||||||
});
|
|
||||||
}); */
|
|
||||||
|
|
||||||
describe("base", () => {
|
|
||||||
it("should connect to the database", async () => {
|
|
||||||
const connection = config.createConnection();
|
|
||||||
expect(await connection.ping()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should clean the database", async () => {
|
|
||||||
const connection = config.createConnection();
|
|
||||||
await cleanDatabase(connection, config);
|
|
||||||
|
|
||||||
const tables = await connection.getIntrospector().getTables();
|
|
||||||
expect(tables).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("integration", () => {
|
|
||||||
let connection: PostgresConnection;
|
|
||||||
beforeAll(async () => {
|
|
||||||
connection = config.createConnection();
|
|
||||||
await cleanDatabase(connection, config);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await cleanDatabase(connection, config);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await connection.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create app and ping", async () => {
|
|
||||||
const app = createApp({
|
|
||||||
connection,
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
expect(app.version()).toBeDefined();
|
|
||||||
expect(await app.em.ping()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a basic schema", async () => {
|
|
||||||
const schema = em(
|
|
||||||
{
|
|
||||||
posts: entity("posts", {
|
|
||||||
title: text().required(),
|
|
||||||
content: text(),
|
|
||||||
}),
|
|
||||||
comments: entity("comments", {
|
|
||||||
content: text(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
(fns, s) => {
|
|
||||||
fns.relation(s.comments).manyToOne(s.posts);
|
|
||||||
fns.index(s.posts).on(["title"], true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
connection,
|
|
||||||
config: {
|
|
||||||
data: schema.toJSON(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
expect(app.em.entities.length).toBe(2);
|
|
||||||
expect(app.em.entities.map((e) => e.name)).toEqual(["posts", "comments"]);
|
|
||||||
|
|
||||||
const api = app.getApi();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
(
|
|
||||||
await api.data.createMany("posts", [
|
|
||||||
{
|
|
||||||
title: "Hello",
|
|
||||||
content: "World",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Hello 2",
|
|
||||||
content: "World 2",
|
|
||||||
},
|
|
||||||
])
|
|
||||||
).data,
|
|
||||||
).toEqual([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "Hello",
|
|
||||||
content: "World",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Hello 2",
|
|
||||||
content: "World 2",
|
|
||||||
},
|
|
||||||
] as any);
|
|
||||||
|
|
||||||
// try to create an existing
|
|
||||||
expect(
|
|
||||||
(
|
|
||||||
await api.data.createOne("posts", {
|
|
||||||
title: "Hello",
|
|
||||||
})
|
|
||||||
).ok,
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
// add a comment to a post
|
|
||||||
await api.data.createOne("comments", {
|
|
||||||
content: "Hello",
|
|
||||||
posts_id: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// and then query using a `with` property
|
|
||||||
const result = await api.data.readMany("posts", { with: ["comments"] });
|
|
||||||
expect(result.length).toBe(2);
|
|
||||||
expect(result[0].comments.length).toBe(1);
|
|
||||||
expect(result[0].comments[0].content).toBe("Hello");
|
|
||||||
expect(result[1].comments.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support uuid", async () => {
|
|
||||||
const schema = em(
|
|
||||||
{
|
|
||||||
posts: entity(
|
|
||||||
"posts",
|
|
||||||
{
|
|
||||||
title: text().required(),
|
|
||||||
content: text(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
primary_format: "uuid",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
comments: entity("comments", {
|
|
||||||
content: text(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
(fns, s) => {
|
|
||||||
fns.relation(s.comments).manyToOne(s.posts);
|
|
||||||
fns.index(s.posts).on(["title"], true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
connection,
|
|
||||||
config: {
|
|
||||||
data: schema.toJSON(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
const config = app.toJSON();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid");
|
|
||||||
|
|
||||||
const $em = app.em;
|
|
||||||
const mutator = $em.mutator($em.entity("posts"));
|
|
||||||
const data = await mutator.insertOne({ title: "Hello", content: "World" });
|
|
||||||
expect(data.data.id).toBeString();
|
|
||||||
expect(String(data.data.id).length).toBe(36);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user