Merge remote-tracking branch 'origin/main' into fork/cameronapak/cp/216-fix-users-link

# Conflicts:
#	bun.lock
This commit is contained in:
dswbx
2025-08-03 12:55:20 +02:00
430 changed files with 14959 additions and 12327 deletions

View File

@@ -20,7 +20,7 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:
bun-version: "1.2.14" bun-version: "1.2.19"
- name: Install dependencies - name: Install dependencies
working-directory: ./app working-directory: ./app

2
.gitignore vendored
View File

@@ -32,3 +32,5 @@ packages/media/.env
docker/tmp docker/tmp
.debug .debug
.history .history
.aider*
.vercel

View File

@@ -1,12 +1,12 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { Guard } from "../../src/auth"; import { Guard } from "../../src/auth/authorize/Guard";
import { parse } from "../../src/core/utils";
import { DataApi } from "../../src/data/api/DataApi"; import { DataApi } from "../../src/data/api/DataApi";
import { DataController } from "../../src/data/api/DataController"; import { DataController } from "../../src/data/api/DataController";
import { dataConfigSchema } from "../../src/data/data-schema"; import { dataConfigSchema } from "../../src/data/data-schema";
import * as proto from "../../src/data/prototype"; import * as proto from "../../src/data/prototype";
import { schemaToEm } from "../helper"; import { schemaToEm } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test"; import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import { parse } from "core/utils/schema";
beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -202,7 +202,7 @@ describe("DataApi", () => {
{ {
// create many // create many
const res = await api.createMany("posts", payload); const res = await api.createMany("posts", payload);
expect(res.data.length).toEqual(4); expect(res.data?.length).toEqual(4);
expect(res.ok).toBeTrue(); expect(res.ok).toBeTrue();
} }

View File

@@ -0,0 +1,37 @@
import { AppServer, serverConfigSchema } from "modules/server/AppServer";
import { describe, test, expect } from "bun:test";
describe("AppServer", () => {
test("config", () => {
{
const server = new AppServer();
expect(server).toBeDefined();
expect(server.config).toEqual({
cors: {
allow_credentials: true,
origin: "*",
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
},
});
}
{
const server = new AppServer({
cors: {
origin: "https",
allow_methods: ["GET", "POST"],
},
});
expect(server).toBeDefined();
expect(server.config).toEqual({
cors: {
allow_credentials: true,
origin: "https",
allow_methods: ["GET", "POST"],
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
},
});
}
});
});

View File

@@ -1,46 +1,3 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Authenticator, type User, type UserPool } from "../../src/auth";
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
import * as hash from "../../src/auth/utils/hash";
import { Default, parse } from "../../src/core/utils";
/*class MemoryUserPool implements UserPool { describe("Authenticator", async () => {});
constructor(private users: User[] = []) {}
async findBy(prop: "id" | "email" | "username", value: string | number) {
return this.users.find((user) => user[prop] === value);
}
async create(user: Pick<User, "email" | "password">) {
const id = this.users.length + 1;
const newUser = { ...user, id, username: user.email };
this.users.push(newUser);
return newUser;
}
}*/
describe("Authenticator", async () => {
test("cookie options", async () => {
console.log("parsed", parse(cookieConfig, undefined));
console.log(Default(cookieConfig, {}));
});
/*const userpool = new MemoryUserPool([
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
]);
test("sha256 login", async () => {
const auth = new Authenticator(userpool, {
password: new PasswordStrategy({
hashing: "sha256",
}),
});
const { token } = await auth.login("password", { email: "d", password: "test" });
expect(token).toBeDefined();
const { iat, ...decoded } = decodeJwt<any>(token);
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
expect(await auth.verify(token)).toBe(true);
});*/
});

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Guard } from "../../../src/auth"; import { Guard } from "../../../src/auth/authorize/Guard";
describe("authorize", () => { describe("authorize", () => {
test("basic", async () => { test("basic", async () => {

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { type TObject, type TString, Type } from "@sinclair/typebox"; import { Registry } from "core/registry/Registry";
import { Registry } from "core"; import { s } from "core/utils/schema";
type Constructor<T> = new (...args: any[]) => T; type Constructor<T> = new (...args: any[]) => T;
@@ -11,7 +11,7 @@ class What {
return null; return null;
} }
getType() { getType() {
return Type.Object({ type: Type.String() }); return s.object({ type: s.string() });
} }
} }
class What2 extends What {} class What2 extends What {}
@@ -19,7 +19,7 @@ class NotAllowed {}
type Test1 = { type Test1 = {
cls: new (...args: any[]) => What; cls: new (...args: any[]) => What;
schema: TObject<{ type: TString }>; schema: s.ObjectSchema<{ type: s.StringSchema }>;
enabled: boolean; enabled: boolean;
}; };
@@ -28,7 +28,7 @@ describe("Registry", () => {
const registry = new Registry<Test1>().set({ const registry = new Registry<Test1>().set({
first: { first: {
cls: What, cls: What,
schema: Type.Object({ type: Type.String(), what: Type.String() }), schema: s.object({ type: s.string(), what: s.string() }),
enabled: true, enabled: true,
}, },
} satisfies Record<string, Test1>); } satisfies Record<string, Test1>);
@@ -37,7 +37,7 @@ describe("Registry", () => {
expect(item).toBeDefined(); expect(item).toBeDefined();
expect(item?.cls).toBe(What); expect(item?.cls).toBe(What);
const second = Type.Object({ type: Type.String(), what: Type.String() }); const second = s.object({ type: s.string(), what: s.string() });
registry.add("second", { registry.add("second", {
cls: What2, cls: What2,
schema: second, schema: second,
@@ -46,7 +46,7 @@ describe("Registry", () => {
// @ts-ignore // @ts-ignore
expect(registry.get("second").schema).toEqual(second); expect(registry.get("second").schema).toEqual(second);
const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() }); const third = s.object({ type: s.string({ default: "1" }), what22: s.string() });
registry.add("third", { registry.add("third", {
// @ts-expect-error // @ts-expect-error
cls: NotAllowed, cls: NotAllowed,
@@ -56,7 +56,7 @@ describe("Registry", () => {
// @ts-ignore // @ts-ignore
expect(registry.get("third").schema).toEqual(third); expect(registry.get("third").schema).toEqual(third);
const fourth = Type.Object({ type: Type.Number(), what22: Type.String() }); const fourth = s.object({ type: s.number(), what22: s.string() });
registry.add("fourth", { registry.add("fourth", {
cls: What, cls: What,
// @ts-expect-error // @ts-expect-error
@@ -81,6 +81,8 @@ describe("Registry", () => {
registry.register("what2", What2); registry.register("what2", What2);
expect(registry.get("what2")).toBeDefined(); expect(registry.get("what2")).toBeDefined();
expect(registry.get("what2").cls).toBe(What2); expect(registry.get("what2").cls).toBe(What2);
expect(registry.get("what2").schema).toEqual(What2.prototype.getType()); expect(JSON.stringify(registry.get("what2").schema)).toEqual(
JSON.stringify(What2.prototype.getType()),
);
}); });
}); });

View File

@@ -1,11 +1,11 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { SchemaObject } from "../../../src/core"; import { s } from "core/utils/schema";
import { Type } from "@sinclair/typebox"; import { SchemaObject } from "core/object/SchemaObject";
describe("SchemaObject", async () => { describe("SchemaObject", async () => {
test("basic", async () => { test("basic", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ a: Type.String({ default: "b" }) }), s.strictObject({ a: s.string({ default: "b" }) }),
{ a: "test" }, { a: "test" },
{ {
forceParse: true, forceParse: true,
@@ -23,19 +23,19 @@ describe("SchemaObject", async () => {
test("patch", async () => { test("patch", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
s: Type.Object( s: s.strictObject(
{ {
a: Type.String({ default: "b" }), a: s.string({ default: "b" }),
b: Type.Object( b: s.strictObject(
{ {
c: Type.String({ default: "d" }), c: s.string({ default: "d" }),
e: Type.String({ default: "f" }), e: s.string({ default: "f" }),
}, },
{ default: {} }, { default: {} },
), ),
}, },
{ default: {}, additionalProperties: false }, { default: {} },
), ),
}), }),
); );
@@ -44,7 +44,7 @@ describe("SchemaObject", async () => {
await m.patch("s.a", "c"); await m.patch("s.a", "c");
// non-existing path on no additional properties // non-existing path on no additional properties
expect(() => m.patch("s.s.s", "c")).toThrow(); expect(m.patch("s.s.s", "c")).rejects.toThrow();
// wrong type // wrong type
expect(() => m.patch("s.a", 1)).toThrow(); expect(() => m.patch("s.a", 1)).toThrow();
@@ -58,8 +58,8 @@ describe("SchemaObject", async () => {
test("patch array", async () => { test("patch array", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }), methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
}), }),
); );
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] }); expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -75,13 +75,13 @@ describe("SchemaObject", async () => {
test("remove", async () => { test("remove", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.object({
s: Type.Object( s: s.object(
{ {
a: Type.String({ default: "b" }), a: s.string({ default: "b" }),
b: Type.Object( b: s.object(
{ {
c: Type.String({ default: "d" }), c: s.string({ default: "d" }),
}, },
{ default: {} }, { default: {} },
), ),
@@ -107,8 +107,8 @@ describe("SchemaObject", async () => {
test("set", async () => { test("set", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }), methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
}), }),
); );
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] }); expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
let called = false; let called = false;
let result: any; let result: any;
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }), methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
}), }),
undefined, undefined,
{ {
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
test("listener: onBeforeUpdate", async () => { test("listener: onBeforeUpdate", async () => {
let called = false; let called = false;
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }), methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
}), }),
undefined, undefined,
{ {
@@ -167,7 +167,7 @@ describe("SchemaObject", async () => {
}); });
test("throwIfRestricted", async () => { test("throwIfRestricted", async () => {
const m = new SchemaObject(Type.Object({}), undefined, { const m = new SchemaObject(s.strictObject({}), undefined, {
restrictPaths: ["a.b"], restrictPaths: ["a.b"],
}); });
@@ -179,13 +179,13 @@ describe("SchemaObject", async () => {
test("restriction bypass", async () => { test("restriction bypass", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
s: Type.Object( s: s.strictObject(
{ {
a: Type.String({ default: "b" }), a: s.string({ default: "b" }),
b: Type.Object( b: s.strictObject(
{ {
c: Type.String({ default: "d" }), c: s.string({ default: "d" }),
}, },
{ default: {} }, { default: {} },
), ),
@@ -205,7 +205,21 @@ describe("SchemaObject", async () => {
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } }); expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
}); });
const dataEntitiesSchema = Type.Object( const dataEntitiesSchema = s.strictObject({
entities: s.record(
s.object({
fields: s.record(
s.object({
type: s.string(),
config: s.object({}).optional(),
}),
),
config: s.record(s.string()).optional(),
}),
),
});
/* const dataEntitiesSchema = Type.Object(
{ {
entities: Type.Object( entities: Type.Object(
{}, {},
@@ -230,7 +244,7 @@ describe("SchemaObject", async () => {
{ {
additionalProperties: false, additionalProperties: false,
}, },
); ); */
test("patch safe object, overwrite", async () => { test("patch safe object, overwrite", async () => {
const data = { const data = {
entities: { entities: {

View File

@@ -1,19 +1,16 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { Guard } from "../../src/auth"; import { Guard } from "../../src/auth/authorize/Guard";
import { parse } from "../../src/core/utils"; import { parse } from "core/utils/schema";
import {
Entity,
type EntityData,
EntityManager,
ManyToOneRelation,
TextField,
} from "../../src/data";
import { DataController } from "../../src/data/api/DataController"; import { DataController } from "../../src/data/api/DataController";
import { dataConfigSchema } from "../../src/data/data-schema"; import { dataConfigSchema } from "../../src/data/data-schema";
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper"; import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult"; import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult"; import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
import { Entity, EntityManager, type EntityData } from "data/entities";
import { TextField } from "data/fields";
import { ManyToOneRelation } from "data/relations";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
beforeAll(() => disableConsoleLog(["log", "warn"])); beforeAll(() => disableConsoleLog(["log", "warn"]));

View File

@@ -1,12 +1,6 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { import { Entity, EntityManager } from "data/entities";
Entity, import { TextField, PrimaryField, NumberField } from "data/fields";
EntityManager,
NumberField,
PrimaryField,
Repository,
TextField,
} from "../../src/data";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();

View File

@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
import type { SqliteDatabase } from "kysely"; import type { SqliteDatabase } from "kysely";
// @ts-ignore // @ts-ignore
import Database from "libsql"; import Database from "libsql";
import { SqliteLocalConnection } from "../../src/data"; import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
export function getDummyDatabase(memory: boolean = true): { export function getDummyDatabase(memory: boolean = true): {
dummyDb: SqliteDatabase; dummyDb: SqliteDatabase;

View File

@@ -1,13 +1,10 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { import { Entity } from "data/entities";
Entity, import { EntityManager } from "data/entities/EntityManager";
EntityManager, import { ManyToOneRelation } from "data/relations";
ManyToOneRelation, import { NumberField, TextField } from "data/fields";
NumberField, import { SchemaManager } from "data/schema/SchemaManager";
SchemaManager,
TextField,
} from "../../src/data";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();

View File

@@ -1,7 +1,8 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data"; import { Entity, EntityManager } from "data/entities";
import { TransformPersistFailedException } from "../../src/data/errors"; import { NumberField, TextField } from "data/fields";
import { TransformPersistFailedException } from "data/errors";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();

View File

@@ -1,6 +1,8 @@
import { afterAll, expect as bunExpect, describe, test } from "bun:test"; import { afterAll, expect as bunExpect, describe, test } from "bun:test";
import { stripMark } from "../../src/core/utils"; import { stripMark } from "core/utils/schema";
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data"; import { Entity, EntityManager } from "data/entities";
import { TextField } from "data/fields";
import { PolymorphicRelation } from "data/relations";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();

View File

@@ -2,19 +2,20 @@ import { describe, expect, test } from "bun:test";
import { import {
BooleanField, BooleanField,
DateField, DateField,
Entity,
EntityIndex,
EntityManager,
EnumField, EnumField,
JsonField, JsonField,
NumberField,
TextField,
EntityIndex,
} from "data/fields";
import { Entity, EntityManager } from "data/entities";
import {
ManyToManyRelation, ManyToManyRelation,
ManyToOneRelation, ManyToOneRelation,
NumberField,
OneToOneRelation, OneToOneRelation,
PolymorphicRelation, PolymorphicRelation,
TextField, } from "data/relations";
} from "../../src/data"; import { DummyConnection } from "data/connection/DummyConnection";
import { DummyConnection } from "../../src/data/connection/DummyConnection";
import { import {
FieldPrototype, FieldPrototype,
type FieldSchema, type FieldSchema,
@@ -32,8 +33,8 @@ import {
number, number,
relation, relation,
text, text,
} from "../../src/data/prototype"; } from "data/prototype";
import { MediaField } from "../../src/media/MediaField"; import { MediaField } from "media/MediaField";
describe("prototype", () => { describe("prototype", () => {
test("...", () => { test("...", () => {
@@ -101,7 +102,8 @@ describe("prototype", () => {
type Posts = Schema<typeof posts2>; type Posts = Schema<typeof posts2>;
expect(posts1.toJSON()).toEqual(posts2.toJSON()); // @todo: check
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
}); });
test("test example", async () => { test("test example", async () => {
@@ -295,9 +297,9 @@ describe("prototype", () => {
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]), new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
new Entity("comments", [new TextField("some")]), new Entity("comments", [new TextField("some")]),
new Entity("users", [new TextField("email")]), new Entity("users", [new TextField("email")]),
]; ] as const;
const _em2 = new EntityManager( const _em2 = new EntityManager(
es, [...es],
new DummyConnection(), new DummyConnection(),
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])], [new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
[ [

View File

@@ -1,13 +1,14 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { Entity, EntityManager, TextField } from "../../src/data"; import { Entity, EntityManager } from "data/entities";
import { TextField } from "data/fields";
import { import {
ManyToManyRelation, ManyToManyRelation,
ManyToOneRelation, ManyToOneRelation,
OneToOneRelation, OneToOneRelation,
PolymorphicRelation, PolymorphicRelation,
RelationField, RelationField,
} from "../../src/data/relations"; } from "data/relations";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
@@ -77,7 +78,7 @@ describe("Relations", async () => {
const em = new EntityManager(entities, dummyConnection, relations); const em = new EntityManager(entities, dummyConnection, relations);
// verify naming // verify naming
const rel = em.relations.all[0]; const rel = em.relations.all[0]!;
expect(rel.source.entity.name).toBe(posts.name); expect(rel.source.entity.name).toBe(posts.name);
expect(rel.source.reference).toBe(posts.name); expect(rel.source.reference).toBe(posts.name);
expect(rel.target.entity.name).toBe(users.name); expect(rel.target.entity.name).toBe(users.name);
@@ -89,11 +90,11 @@ describe("Relations", async () => {
// verify low level relation // verify low level relation
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts); expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
expect(posts.field("author_id")).toBeInstanceOf(RelationField); expect(posts.field("author_id")).toBeInstanceOf(RelationField);
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts); expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
// verify high level relation (from users) // verify high level relation (from users)
const userPostsRel = em.relationOf(users.name, "posts"); const userPostsRel = em.relationOf(users.name, "posts");
@@ -191,7 +192,7 @@ describe("Relations", async () => {
const em = new EntityManager(entities, dummyConnection, relations); const em = new EntityManager(entities, dummyConnection, relations);
// verify naming // verify naming
const rel = em.relations.all[0]; const rel = em.relations.all[0]!;
expect(rel.source.entity.name).toBe(users.name); expect(rel.source.entity.name).toBe(users.name);
expect(rel.source.reference).toBe(users.name); expect(rel.source.reference).toBe(users.name);
expect(rel.target.entity.name).toBe(settings.name); expect(rel.target.entity.name).toBe(settings.name);
@@ -202,8 +203,8 @@ describe("Relations", async () => {
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name).length).toBe(1); expect(em.relationsOf(users.name).length).toBe(1);
expect(em.relationsOf(users.name)[0].source.entity).toBe(users); expect(em.relationsOf(users.name)[0]!.source.entity).toBe(users);
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings); expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings);
// verify high level relation (from users) // verify high level relation (from users)
const userSettingRel = em.relationOf(users.name, settings.name); const userSettingRel = em.relationOf(users.name, settings.name);
@@ -323,7 +324,7 @@ describe("Relations", async () => {
); );
// mutation info // mutation info
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({ expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({
reference: "categories", reference: "categories",
local_field: undefined, local_field: undefined,
$set: false, $set: false,
@@ -334,7 +335,7 @@ describe("Relations", async () => {
cardinality: undefined, cardinality: undefined,
relation_type: "m:n", relation_type: "m:n",
}); });
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({ expect(relations[0]!.helper(categories.name)!.getMutationInfo()).toEqual({
reference: "posts", reference: "posts",
local_field: undefined, local_field: undefined,
$set: false, $set: false,

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Entity, NumberField, TextField } from "data"; import { Entity } from "data/entities";
import * as p from "data/prototype"; import { NumberField, TextField } from "data/fields";
describe("[data] Entity", async () => { describe("[data] Entity", async () => {
const entity = new Entity("test", [ const entity = new Entity("test", [

View File

@@ -1,12 +1,8 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { import { Entity, EntityManager } from "data/entities";
Entity, import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
EntityManager, import { SchemaManager } from "data/schema/SchemaManager";
ManyToManyRelation, import { UnableToConnectException } from "data/errors";
ManyToOneRelation,
SchemaManager,
} from "../../../src/data";
import { UnableToConnectException } from "../../../src/data/errors";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();

View File

@@ -1,6 +1,8 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data"; import { Entity, EntityManager } from "data/entities";
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder"; import { ManyToOneRelation } from "data/relations";
import { TextField } from "data/fields";
import { JoinBuilder } from "data/entities/query/JoinBuilder";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();

View File

@@ -1,18 +1,16 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { EventManager } from "../../../src/core/events"; import type { EventManager } from "../../../src/core/events";
import { Entity, EntityManager } from "data/entities";
import { import {
Entity,
EntityManager,
ManyToOneRelation, ManyToOneRelation,
MutatorEvents,
NumberField,
OneToOneRelation, OneToOneRelation,
type RelationField, RelationField,
RelationMutator, RelationMutator,
TextField, } from "data/relations";
} from "../../../src/data"; import { NumberField, TextField } from "data/fields";
import * as proto from "../../../src/data/prototype"; import * as proto from "data/prototype";
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper"; import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
import { MutatorEvents } from "data/events";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterAll(afterAllCleanup); afterAll(afterAllCleanup);

View File

@@ -1,17 +1,10 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import type { Kysely, Transaction } from "kysely"; import type { Kysely, Transaction } from "kysely";
import { Perf } from "core/utils"; import { TextField } from "data/fields";
import { import { em as $em, entity as $entity, text as $text } from "data/prototype";
Entity, import { Entity, EntityManager } from "data/entities";
EntityManager, import { ManyToOneRelation } from "data/relations";
LibsqlConnection, import { RepositoryEvents } from "data/events";
ManyToOneRelation,
RepositoryEvents,
TextField,
entity as $entity,
text as $text,
em as $em,
} from "data";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
type E = Kysely<any> | Transaction<any>; type E = Kysely<any> | Transaction<any>;

View File

@@ -1,7 +1,9 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { randomString } from "../../../src/core/utils"; import { randomString } from "core/utils";
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data"; import { Entity, EntityManager } from "data/entities";
import { TextField, EntityIndex } from "data/fields";
import { SchemaManager } from "data/schema/SchemaManager";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();

View File

@@ -1,6 +1,6 @@
import { describe, test, expect } from "bun:test"; import { describe, test, expect } from "bun:test";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { type WhereQuery, WhereBuilder } from "data"; import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
function qb() { function qb() {
const c = getDummyConnection(); const c = getDummyConnection();

View File

@@ -1,14 +1,9 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { import { Entity, EntityManager } from "data/entities";
Entity, import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
EntityManager, import { TextField } from "data/fields";
ManyToManyRelation, import * as proto from "data/prototype";
ManyToOneRelation, import { WithBuilder } from "data/entities/query/WithBuilder";
PolymorphicRelation,
TextField,
WithBuilder,
} from "../../../src/data";
import * as proto from "../../../src/data/prototype";
import { schemaToEm } from "../../helper"; import { schemaToEm } from "../../helper";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";

View File

@@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { EntityManager } from "../../../../src/data"; import { EntityManager } from "data/entities/EntityManager";
import { getDummyConnection } from "../../helper"; import { getDummyConnection } from "../../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();

View File

@@ -1,9 +1,10 @@
import { bunTestRunner } from "adapter/bun/test";
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { BooleanField } from "../../../../src/data"; import { BooleanField } from "data/fields";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
describe("[data] BooleanField", async () => { describe("[data] BooleanField", async () => {
fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" }); fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
test("transformRetrieve", async () => { test("transformRetrieve", async () => {
const field = new BooleanField("test"); const field = new BooleanField("test");

View File

@@ -1,9 +1,15 @@
import { describe, expect, test } from "bun:test"; import { describe, test } from "bun:test";
import { DateField } from "../../../../src/data"; import { DateField } from "data/fields";
import { fieldTestSuite } from "data/fields/field-test-suite"; import { fieldTestSuite } from "data/fields/field-test-suite";
import { bunTestRunner } from "adapter/bun/test";
describe("[data] DateField", async () => { describe("[data] DateField", async () => {
fieldTestSuite({ expect, test }, DateField, { defaultValue: new Date(), schemaType: "date" }); fieldTestSuite(
bunTestRunner,
DateField,
{ defaultValue: new Date(), schemaType: "date" },
{ type: "date" },
);
// @todo: add datefield tests // @todo: add datefield tests
test("week", async () => { test("week", async () => {

View File

@@ -1,5 +1,6 @@
import { bunTestRunner } from "adapter/bun/test";
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { EnumField } from "../../../../src/data"; import { EnumField } from "data/fields";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
function options(strings: string[]) { function options(strings: string[]) {
@@ -8,7 +9,7 @@ function options(strings: string[]) {
describe("[data] EnumField", async () => { describe("[data] EnumField", async () => {
fieldTestSuite( fieldTestSuite(
{ expect, test }, bunTestRunner,
// @ts-ignore // @ts-ignore
EnumField, EnumField,
{ defaultValue: "a", schemaType: "text" }, { defaultValue: "a", schemaType: "text" },

View File

@@ -1,7 +1,8 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Default, stripMark } from "../../../../src/core/utils";
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field"; import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
import { fieldTestSuite } from "data/fields/field-test-suite"; import { fieldTestSuite } from "data/fields/field-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import { stripMark } from "core/utils/schema";
describe("[data] Field", async () => { describe("[data] Field", async () => {
class FieldSpec extends Field { class FieldSpec extends Field {
@@ -19,10 +20,10 @@ describe("[data] Field", async () => {
}); });
}); });
fieldTestSuite({ expect, test }, FieldSpec, { defaultValue: "test", schemaType: "text" }); fieldTestSuite(bunTestRunner, FieldSpec, { defaultValue: "test", schemaType: "text" });
test("default config", async () => { test("default config", async () => {
const config = Default(baseFieldConfigSchema, {}); const config = baseFieldConfigSchema.template({});
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any); expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
}); });

View File

@@ -1,10 +1,11 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Type } from "@sinclair/typebox"; import { Entity } from "data/entities";
import { Entity, EntityIndex, Field } from "../../../../src/data"; import { Field, EntityIndex } from "data/fields";
import { s } from "core/utils/schema";
class TestField extends Field { class TestField extends Field {
protected getSchema(): any { protected getSchema(): any {
return Type.Any(); return s.any();
} }
override schema() { override schema() {

View File

@@ -1,10 +1,11 @@
import { bunTestRunner } from "adapter/bun/test";
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { JsonField } from "../../../../src/data"; import { JsonField } from "data/fields";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
describe("[data] JsonField", async () => { describe("[data] JsonField", async () => {
const field = new JsonField("test"); const field = new JsonField("test");
fieldTestSuite({ expect, test }, JsonField, { fieldTestSuite(bunTestRunner, JsonField, {
defaultValue: { a: 1 }, defaultValue: { a: 1 },
sampleValues: ["string", { test: 1 }, 1], sampleValues: ["string", { test: 1 }, 1],
schemaType: "text", schemaType: "text",

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { JsonSchemaField } from "../../../../src/data"; import { JsonSchemaField } from "data/fields";
import { fieldTestSuite } from "data/fields/field-test-suite"; import { fieldTestSuite } from "data/fields/field-test-suite";
describe("[data] JsonSchemaField", async () => { describe("[data] JsonSchemaField", async () => {

View File

@@ -1,5 +1,6 @@
import { bunTestRunner } from "adapter/bun/test";
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { NumberField } from "../../../../src/data"; import { NumberField } from "data/fields";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
describe("[data] NumberField", async () => { describe("[data] NumberField", async () => {
@@ -15,5 +16,5 @@ describe("[data] NumberField", async () => {
expect(transformPersist(field2, 10000)).resolves.toBe(10000); expect(transformPersist(field2, 10000)).resolves.toBe(10000);
}); });
fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" }); fieldTestSuite(bunTestRunner, NumberField, { defaultValue: 12, schemaType: "integer" });
}); });

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { PrimaryField } from "../../../../src/data"; import { PrimaryField } from "data/fields";
describe("[data] PrimaryField", async () => { describe("[data] PrimaryField", async () => {
const field = new PrimaryField("primary"); const field = new PrimaryField("primary");

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { TextField } from "../../../../src/data"; import { TextField } from "data/fields";
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
import { bunTestRunner } from "adapter/bun/test";
describe("[data] TextField", async () => { describe("[data] TextField", async () => {
test("transformPersist (config)", async () => { test("transformPersist (config)", async () => {
@@ -11,5 +12,5 @@ describe("[data] TextField", async () => {
expect(transformPersist(field, "abc")).resolves.toBe("abc"); expect(transformPersist(field, "abc")).resolves.toBe("abc");
}); });
fieldTestSuite({ expect, test }, TextField, { defaultValue: "abc", schemaType: "text" }); fieldTestSuite(bunTestRunner, TextField, { defaultValue: "abc", schemaType: "text" });
}); });

View File

@@ -1,11 +1,11 @@
import { describe, expect, it, test } from "bun:test"; import { describe, expect, it, test } from "bun:test";
import { Entity, type EntityManager } from "../../../../src/data"; import { Entity, type EntityManager } from "data/entities";
import { import {
type BaseRelationConfig, type BaseRelationConfig,
EntityRelation, EntityRelation,
EntityRelationAnchor, EntityRelationAnchor,
RelationTypes, RelationTypes,
} from "../../../../src/data/relations"; } from "data/relations";
class TestEntityRelation extends EntityRelation { class TestEntityRelation extends EntityRelation {
constructor(config?: BaseRelationConfig) { constructor(config?: BaseRelationConfig) {
@@ -24,11 +24,11 @@ class TestEntityRelation extends EntityRelation {
return this; return this;
} }
buildWith(a: any, b: any, c: any): any { buildWith(): any {
return; return;
} }
buildJoin(a: any, b: any): any { buildJoin(): any {
return; return;
} }
} }

View File

@@ -41,7 +41,7 @@ beforeAll(() =>
); );
afterAll(unmockFetch); afterAll(unmockFetch);
describe("FetchTask", async () => { describe.skip("FetchTask", async () => {
test("Simple fetch", async () => { test("Simple fetch", async () => {
const task = new FetchTask("Fetch Something", { const task = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1", url: "https://jsonplaceholder.typicode.com/todos/1",

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows"; import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
import { Type } from "@sinclair/typebox"; import { s } from "core/utils/schema";
export class StringifyTask<Output extends string> extends Task< export class StringifyTask<Output extends string> extends Task<
typeof StringifyTask.schema, typeof StringifyTask.schema,
@@ -8,18 +8,16 @@ export class StringifyTask<Output extends string> extends Task<
> { > {
type = "stringify"; type = "stringify";
static override schema = Type.Optional( static override schema = s.object({
Type.Object({ input: s.string().optional(),
input: Type.Optional(Type.String()), });
}),
);
async execute() { async execute() {
return JSON.stringify(this.params.input) as Output; return JSON.stringify(this.params.input) as Output;
} }
} }
describe("SubFlowTask", async () => { describe.skip("SubFlowTask", async () => {
test("Simple Subflow", async () => { test("Simple Subflow", async () => {
const subTask = new RenderTask("render", { const subTask = new RenderTask("render", {
render: "subflow", render: "subflow",

View File

@@ -1,12 +1,12 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Type } from "@sinclair/typebox";
import { Task } from "../../src/flows"; import { Task } from "../../src/flows";
import { dynamic } from "../../src/flows/tasks/Task"; import { dynamic } from "../../src/flows/tasks/Task";
import { s } from "core/utils/schema";
describe("Task", async () => { describe.skip("Task", async () => {
test("resolveParams: template with parse", async () => { test("resolveParams: template with parse", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Number()) }), s.object({ test: dynamic(s.number()) }),
{ {
test: "{{ some.path }}", test: "{{ some.path }}",
}, },
@@ -22,7 +22,7 @@ describe("Task", async () => {
test("resolveParams: with string", async () => { test("resolveParams: with string", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ test: Type.String() }), s.object({ test: s.string() }),
{ {
test: "{{ some.path }}", test: "{{ some.path }}",
}, },
@@ -38,7 +38,7 @@ describe("Task", async () => {
test("resolveParams: with object", async () => { test("resolveParams: with object", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }), s.object({ test: dynamic(s.object({ key: s.string(), value: s.string() })) }),
{ {
test: { key: "path", value: "{{ some.path }}" }, test: { key: "path", value: "{{ some.path }}" },
}, },

View File

@@ -1,8 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Hono } from "hono"; import { Hono } from "hono";
import { Event, EventManager } from "../../src/core/events"; import { Event, EventManager } from "../../src/core/events";
import { parse } from "../../src/core/utils"; import { s, parse } from "core/utils/schema";
import { type Static, type StaticDecode, Type } from "@sinclair/typebox";
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows"; import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
import { dynamic } from "../../src/flows/tasks/Task"; import { dynamic } from "../../src/flows/tasks/Task";
@@ -15,15 +14,15 @@ class Passthrough extends Task {
} }
} }
type OutputIn = Static<typeof OutputParamTask.schema>; type OutputIn = s.Static<typeof OutputParamTask.schema>;
type OutputOut = StaticDecode<typeof OutputParamTask.schema>; type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
class OutputParamTask extends Task<typeof OutputParamTask.schema> { class OutputParamTask extends Task<typeof OutputParamTask.schema> {
type = "output-param"; type = "output-param";
static override schema = Type.Object({ static override schema = s.strictObject({
number: dynamic( number: dynamic(
Type.Number({ s.number({
title: "Output number", title: "Output number",
}), }),
Number.parseInt, Number.parseInt,
@@ -44,7 +43,7 @@ class PassthroughFlowInput extends Task {
} }
} }
describe("Flow task inputs", async () => { describe.skip("Flow task inputs", async () => {
test("types", async () => { test("types", async () => {
const schema = OutputParamTask.schema; const schema = OutputParamTask.schema;

View File

@@ -30,7 +30,7 @@ class ExecTask extends Task {
} }
} }
describe("Flow trigger", async () => { describe.skip("Flow trigger", async () => {
test("manual trigger", async () => { test("manual trigger", async () => {
let called = false; let called = false;

View File

@@ -2,7 +2,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { isEqual } from "lodash-es"; import { isEqual } from "lodash-es";
import { _jsonp, withDisabledConsole } from "../../src/core/utils"; import { _jsonp, withDisabledConsole } from "../../src/core/utils";
import { type Static, Type } from "@sinclair/typebox"; import { s } from "core/utils/schema";
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows"; import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
/*beforeAll(disableConsoleLog); /*beforeAll(disableConsoleLog);
@@ -11,19 +11,19 @@ afterAll(enableConsoleLog);*/
class ExecTask extends Task<typeof ExecTask.schema> { class ExecTask extends Task<typeof ExecTask.schema> {
type = "exec"; type = "exec";
static override schema = Type.Object({ static override schema = s.object({
delay: Type.Number({ default: 10 }), delay: s.number({ default: 10 }),
}); });
constructor( constructor(
name: string, name: string,
params: Static<typeof ExecTask.schema>, params: s.Static<typeof ExecTask.schema>,
private func: () => Promise<any>, private func: () => Promise<any>,
) { ) {
super(name, params); super(name, params);
} }
override clone(name: string, params: Static<typeof ExecTask.schema>) { override clone(name: string, params: s.Static<typeof ExecTask.schema>) {
return new ExecTask(name, params, this.func); return new ExecTask(name, params, this.func);
} }
@@ -78,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
return diff; return diff;
} }
describe("Flow tests", async () => { describe.skip("Flow tests", async () => {
test("Simple single task", async () => { test("Simple single task", async () => {
const simple = getTask(0); const simple = getTask(0);

View File

@@ -2,11 +2,12 @@ import { unlink } from "node:fs/promises";
import type { SelectQueryBuilder, SqliteDatabase } from "kysely"; import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
import Database from "libsql"; import Database from "libsql";
import { format as sqlFormat } from "sql-formatter"; import { format as sqlFormat } from "sql-formatter";
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
import type { em as protoEm } from "../src/data/prototype"; import type { em as protoEm } from "../src/data/prototype";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { slugify } from "core/utils/strings"; import { slugify } from "core/utils/strings";
import { type Connection, SqliteLocalConnection } from "data/connection";
import { EntityManager } from "data/entities/EntityManager";
export function getDummyDatabase(memory: boolean = true): { export function getDummyDatabase(memory: boolean = true): {
dummyDb: SqliteDatabase; dummyDb: SqliteDatabase;

View File

@@ -13,9 +13,8 @@ describe("integration config", () => {
// create entity // create entity
await api.system.addConfig("data", "entities.posts", { await api.system.addConfig("data", "entities.posts", {
name: "posts",
config: { sort_field: "id", sort_dir: "asc" }, config: { sort_field: "id", sort_dir: "asc" },
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } }, fields: { id: { type: "primary" }, asdf: { type: "text" } },
type: "regular", type: "regular",
}); });

View File

@@ -46,7 +46,6 @@ afterAll(enableConsoleLog);
describe("MediaController", () => { describe("MediaController", () => {
test("accepts direct", async () => { test("accepts direct", async () => {
const app = await makeApp(); const app = await makeApp();
console.log("app", app);
const file = Bun.file(path); const file = Bun.file(path);
const name = makeName("png"); const name = makeName("png");
@@ -55,7 +54,6 @@ describe("MediaController", () => {
body: file, body: file,
}); });
const result = (await res.json()) as any; const result = (await res.json()) as any;
console.log(result);
expect(result.name).toBe(name); expect(result.name).toBe(name);
const destFile = Bun.file(assetsTmpPath + "/" + name); const destFile = Bun.file(assetsTmpPath + "/" + name);

View File

@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { type FileBody, Storage } from "../../src/media/storage/Storage"; import { type FileBody, Storage } from "../../src/media/storage/Storage";
import * as StorageEvents from "../../src/media/storage/events"; import * as StorageEvents from "../../src/media/storage/events";
import { StorageAdapter } from "media"; import { StorageAdapter } from "media/storage/StorageAdapter";
class TestAdapter extends StorageAdapter { class TestAdapter extends StorageAdapter {
files: Record<string, FileBody> = {}; files: Record<string, FileBody> = {};

View File

@@ -1,13 +1,18 @@
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test"; import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import { AuthController } from "../../src/auth/api/AuthController"; import { AuthController } from "../../src/auth/api/AuthController";
import { em, entity, make, text } from "../../src/data"; import { em, entity, make, text } from "data/prototype";
import { AppAuth, type ModuleBuildContext } from "../../src/modules"; import { AppAuth, type ModuleBuildContext } from "modules";
import { disableConsoleLog, enableConsoleLog } from "../helper"; import { disableConsoleLog, enableConsoleLog } from "../helper";
// @ts-ignore
import { makeCtx, moduleTestSuite } from "./module-test-suite"; import { makeCtx, moduleTestSuite } from "./module-test-suite";
describe("AppAuth", () => { describe("AppAuth", () => {
test.only("...", () => {
const auth = new AppAuth({});
console.log(auth.toJSON());
console.log(auth.config);
});
moduleTestSuite(AppAuth); moduleTestSuite(AppAuth);
let ctx: ModuleBuildContext; let ctx: ModuleBuildContext;

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, test } from "bun:test"; import { beforeEach, describe, expect, test } from "bun:test";
import { parse } from "../../src/core/utils"; import { parse } from "core/utils/schema";
import { fieldsSchema } from "../../src/data/data-schema"; import { fieldsSchema } from "../../src/data/data-schema";
import { AppData, type ModuleBuildContext } from "../../src/modules"; import { AppData, type ModuleBuildContext } from "../../src/modules";
import { makeCtx, moduleTestSuite } from "./module-test-suite"; import { makeCtx, moduleTestSuite } from "./module-test-suite";

View File

@@ -1,12 +1,17 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { registries } from "../../src";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import { em, entity, text } from "../../src/data"; import { em, entity, text } from "data/prototype";
import { registries } from "modules/registries";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { AppMedia } from "../../src/modules"; import { AppMedia } from "../../src/media/AppMedia";
import { moduleTestSuite } from "./module-test-suite"; import { moduleTestSuite } from "./module-test-suite";
describe("AppMedia", () => { describe("AppMedia", () => {
test.only("...", () => {
const media = new AppMedia();
console.log(media.toJSON());
});
moduleTestSuite(AppMedia); moduleTestSuite(AppMedia);
test("should allow additional fields", async () => { test("should allow additional fields", async () => {

View File

@@ -1,13 +1,13 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { stripMark } from "../../src/core/utils"; import { s, stripMark } from "core/utils/schema";
import { type TSchema, Type } from "@sinclair/typebox"; import { em, entity, index, text } from "data/prototype";
import { EntityManager, em, entity, index, text } from "../../src/data"; import { EntityManager } from "data/entities/EntityManager";
import { DummyConnection } from "../../src/data/connection/DummyConnection"; import { DummyConnection } from "../../src/data/connection/DummyConnection";
import { Module } from "../../src/modules/Module"; import { Module } from "../../src/modules/Module";
import { ModuleHelper } from "modules/ModuleHelper"; import { ModuleHelper } from "modules/ModuleHelper";
function createModule<Schema extends TSchema>(schema: Schema) { function createModule<Schema extends s.Schema>(schema: Schema) {
class TestModule extends Module<typeof schema> { return class TestModule extends Module<Schema> {
getSchema() { getSchema() {
return schema; return schema;
} }
@@ -17,9 +17,7 @@ function createModule<Schema extends TSchema>(schema: Schema) {
override useForceParse() { override useForceParse() {
return true; return true;
} }
} };
return TestModule;
} }
describe("Module", async () => { describe("Module", async () => {
@@ -27,7 +25,7 @@ describe("Module", async () => {
test("listener", async () => { test("listener", async () => {
let result: any; let result: any;
const module = createModule(Type.Object({ a: Type.String() })); const module = createModule(s.object({ a: s.string() }));
const m = new module({ a: "test" }); const m = new module({ a: "test" });
await m.schema().set({ a: "test2" }); await m.schema().set({ a: "test2" });
@@ -43,7 +41,7 @@ describe("Module", async () => {
describe("db schema", () => { describe("db schema", () => {
class M extends Module { class M extends Module {
override getSchema() { override getSchema() {
return Type.Object({}); return s.object({});
} }
prt = { prt = {

View File

@@ -1,13 +1,13 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { disableConsoleLog, enableConsoleLog, stripMark } from "core/utils"; import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { Type } from "@sinclair/typebox";
import { Connection, entity, text } from "data";
import { Module } from "modules/Module"; import { Module } from "modules/Module";
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager"; import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations"; import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { diff } from "core/object/diff"; import { s, stripMark } from "core/utils/schema";
import type { Static } from "@sinclair/typebox"; import { Connection } from "data/connection/Connection";
import { entity, text } from "data/prototype";
describe("ModuleManager", async () => { describe("ModuleManager", async () => {
test("s1: no config, no build", async () => { test("s1: no config, no build", async () => {
@@ -92,7 +92,11 @@ describe("ModuleManager", async () => {
await mm2.build(); await mm2.build();
expect(stripMark(json)).toEqual(stripMark(mm2.configs())); /* console.log({
json,
configs: mm2.configs(),
}); */
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
expect(mm2.configs().data.entities?.test).toBeDefined(); expect(mm2.configs().data.entities?.test).toBeDefined();
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined(); expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined(); expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
@@ -257,10 +261,10 @@ describe("ModuleManager", async () => {
// @todo: add tests for migrations (check "backup" and new version) // @todo: add tests for migrations (check "backup" and new version)
describe("revert", async () => { describe("revert", async () => {
const failingModuleSchema = Type.Object({ const failingModuleSchema = s.partialObject({
value: Type.Optional(Type.Number()), value: s.number(),
}); });
class FailingModule extends Module<typeof failingModuleSchema> { class FailingModule extends Module<s.Static<typeof failingModuleSchema>> {
getSchema() { getSchema() {
return failingModuleSchema; return failingModuleSchema;
} }
@@ -431,11 +435,11 @@ describe("ModuleManager", async () => {
}); });
describe("validate & revert", () => { describe("validate & revert", () => {
const schema = Type.Object({ const schema = s.object({
value: Type.Array(Type.Number(), { default: [] }), value: s.array(s.number()),
}); });
type SampleSchema = Static<typeof schema>; type SampleSchema = s.Static<typeof schema>;
class Sample extends Module<typeof schema> { class Sample extends Module<SampleSchema> {
getSchema() { getSchema() {
return schema; return schema;
} }

View File

@@ -1,12 +1,11 @@
import { beforeEach, describe, expect, it } from "bun:test"; import { beforeEach, describe, expect, it } from "bun:test";
import { Hono } from "hono"; import { Hono } from "hono";
import { Guard } from "../../src/auth"; import { Guard } from "auth/authorize/Guard";
import { DebugLogger } from "../../src/core"; import { DebugLogger } from "core/utils/DebugLogger";
import { EventManager } from "../../src/core/events"; import { EventManager } from "core/events";
import { Default, stripMark } from "../../src/core/utils"; import { EntityManager } from "data/entities/EntityManager";
import { EntityManager } from "../../src/data"; import { Module, type ModuleBuildContext } from "modules/Module";
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { ModuleHelper } from "modules/ModuleHelper"; import { ModuleHelper } from "modules/ModuleHelper";
@@ -45,7 +44,8 @@ export function moduleTestSuite(module: { new (): Module }) {
it("uses the default config", async () => { it("uses the default config", async () => {
const m = new module(); const m = new module();
await m.setContext(ctx).build(); await m.setContext(ctx).build();
expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {})); expect(m.toJSON()).toEqual(m.getSchema().template({}, { withOptional: true }));
//expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
}); });
}); });
} }

View File

@@ -1,6 +1,7 @@
import { $ } from "bun"; import { $ } from "bun";
import * as tsup from "tsup"; import * as tsup from "tsup";
import pkg from "./package.json" with { type: "json" }; import pkg from "./package.json" with { type: "json" };
import c from "picocolors";
const args = process.argv.slice(2); const args = process.argv.slice(2);
const watch = args.includes("--watch"); const watch = args.includes("--watch");
@@ -9,6 +10,14 @@ const types = args.includes("--types");
const sourcemap = args.includes("--sourcemap"); const sourcemap = args.includes("--sourcemap");
const clean = args.includes("--clean"); const clean = args.includes("--clean");
// silence tsup
const oldConsole = {
log: console.log,
warn: console.warn,
};
console.log = () => {};
console.warn = () => {};
const define = { const define = {
__isDev: "0", __isDev: "0",
__version: JSON.stringify(pkg.version), __version: JSON.stringify(pkg.version),
@@ -27,11 +36,11 @@ function buildTypes() {
Bun.spawn(["bun", "build:types"], { Bun.spawn(["bun", "build:types"], {
stdout: "inherit", stdout: "inherit",
onExit: () => { onExit: () => {
console.info("Types built"); oldConsole.log(c.cyan("[Types]"), c.green("built"));
Bun.spawn(["bun", "tsc-alias"], { Bun.spawn(["bun", "tsc-alias"], {
stdout: "inherit", stdout: "inherit",
onExit: () => { onExit: () => {
console.info("Types aliased"); oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
types_running = false; types_running = false;
}, },
}); });
@@ -39,6 +48,10 @@ function buildTypes() {
}); });
} }
if (types && !watch) {
buildTypes();
}
let watcher_timeout: any; let watcher_timeout: any;
function delayTypes() { function delayTypes() {
if (!watch || !types) return; if (!watch || !types) return;
@@ -48,17 +61,6 @@ function delayTypes() {
watcher_timeout = setTimeout(buildTypes, 1000); watcher_timeout = setTimeout(buildTypes, 1000);
} }
if (types && !watch) {
buildTypes();
}
function banner(title: string) {
console.info("");
console.info("=".repeat(40));
console.info(title.toUpperCase());
console.info("-".repeat(40));
}
// collection of always-external packages // collection of always-external packages
const external = [ const external = [
"bun:test", "bun:test",
@@ -73,20 +75,12 @@ const external = [
* Building backend and general API * Building backend and general API
*/ */
async function buildApi() { async function buildApi() {
banner("Building API");
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
watch, watch,
define, define,
entry: [ entry: ["src/index.ts", "src/core/utils/index.ts", "src/plugins/index.ts"],
"src/index.ts",
"src/core/index.ts",
"src/core/utils/index.ts",
"src/data/index.ts",
"src/media/index.ts",
"src/plugins/index.ts",
],
outDir: "dist", outDir: "dist",
external: [...external], external: [...external],
metafile: true, metafile: true,
@@ -99,6 +93,7 @@ async function buildApi() {
}, },
onSuccess: async () => { onSuccess: async () => {
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[API]"), c.green("built"));
}, },
}); });
} }
@@ -142,7 +137,6 @@ async function buildUi() {
}, },
} satisfies tsup.Options; } satisfies tsup.Options;
banner("Building UI");
await tsup.build({ await tsup.build({
...base, ...base,
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"], entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
@@ -150,10 +144,10 @@ async function buildUi() {
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/index.js"); await rewriteClient("./dist/ui/index.js");
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[UI]"), c.green("built"));
}, },
}); });
banner("Building Client");
await tsup.build({ await tsup.build({
...base, ...base,
entry: ["src/ui/client/index.ts"], entry: ["src/ui/client/index.ts"],
@@ -161,6 +155,7 @@ async function buildUi() {
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/client/index.js"); await rewriteClient("./dist/ui/client/index.js");
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
}, },
}); });
} }
@@ -171,7 +166,6 @@ async function buildUi() {
* - ui/client is external, and after built replaced with "bknd/client" * - ui/client is external, and after built replaced with "bknd/client"
*/ */
async function buildUiElements() { async function buildUiElements() {
banner("Building UI Elements");
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
@@ -205,6 +199,7 @@ async function buildUiElements() {
onSuccess: async () => { onSuccess: async () => {
await rewriteClient("./dist/ui/elements/index.js"); await rewriteClient("./dist/ui/elements/index.js");
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
}, },
}); });
} }
@@ -225,6 +220,7 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
splitting: false, splitting: false,
onSuccess: async () => { onSuccess: async () => {
delayTypes(); delayTypes();
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
}, },
...overrides, ...overrides,
define: { define: {
@@ -233,7 +229,7 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
}, },
external: [ external: [
/^cloudflare*/, /^cloudflare*/,
/^@?(hono).*?/, /^@?hono.*?/,
/^(bknd|react|next|node).*?/, /^(bknd|react|next|node).*?/,
/.*\.(html)$/, /.*\.(html)$/,
...external, ...external,
@@ -243,65 +239,63 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
} }
async function buildAdapters() { async function buildAdapters() {
banner("Building Adapters"); await Promise.all([
// base adapter handles // base adapter handles
await tsup.build({ tsup.build({
...baseConfig(""), ...baseConfig(""),
entry: ["src/adapter/index.ts"], entry: ["src/adapter/index.ts"],
outDir: "dist/adapter", outDir: "dist/adapter",
}); }),
// specific adatpers // specific adatpers
await tsup.build(baseConfig("react-router")); tsup.build(baseConfig("react-router")),
await tsup.build( tsup.build(
baseConfig("bun", { baseConfig("bun", {
external: [/^bun\:.*/],
}),
),
tsup.build(baseConfig("astro")),
tsup.build(baseConfig("aws")),
tsup.build(baseConfig("cloudflare")),
tsup.build({
...baseConfig("vite"),
platform: "node",
}),
tsup.build({
...baseConfig("nextjs"),
platform: "node",
}),
tsup.build({
...baseConfig("node"),
platform: "node",
}),
tsup.build({
...baseConfig("sqlite/edge"),
entry: ["src/adapter/sqlite/edge.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
}),
tsup.build({
...baseConfig("sqlite/node"),
entry: ["src/adapter/sqlite/node.ts"],
outDir: "dist/adapter/sqlite",
platform: "node",
metafile: false,
}),
tsup.build({
...baseConfig("sqlite/bun"),
entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
external: [/^bun\:.*/], external: [/^bun\:.*/],
}), }),
); ]);
await tsup.build(baseConfig("astro"));
await tsup.build(baseConfig("aws"));
await tsup.build(baseConfig("cloudflare"));
await tsup.build({
...baseConfig("vite"),
platform: "node",
});
await tsup.build({
...baseConfig("nextjs"),
platform: "node",
});
await tsup.build({
...baseConfig("node"),
platform: "node",
});
await tsup.build({
...baseConfig("sqlite/edge"),
entry: ["src/adapter/sqlite/edge.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
});
await tsup.build({
...baseConfig("sqlite/node"),
entry: ["src/adapter/sqlite/node.ts"],
outDir: "dist/adapter/sqlite",
platform: "node",
metafile: false,
});
await tsup.build({
...baseConfig("sqlite/bun"),
entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite",
metafile: false,
external: [/^bun\:.*/],
});
} }
await buildApi(); await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
await buildUi();
await buildUiElements();
await buildAdapters();

View File

@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.15.0", "version": "0.16.0",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -13,6 +13,7 @@
"bugs": { "bugs": {
"url": "https://github.com/bknd-io/bknd/issues" "url": "https://github.com/bknd-io/bknd/issues"
}, },
"packageManager": "bun@1.2.19",
"engines": { "engines": {
"node": ">=22" "node": ">=22"
}, },
@@ -54,7 +55,6 @@
"@hono/swagger-ui": "^0.5.1", "@hono/swagger-ui": "^0.5.1",
"@mantine/core": "^7.17.1", "@mantine/core": "^7.17.1",
"@mantine/hooks": "^7.17.1", "@mantine/hooks": "^7.17.1",
"@sinclair/typebox": "0.34.30",
"@tanstack/react-form": "^1.0.5", "@tanstack/react-form": "^1.0.5",
"@uiw/react-codemirror": "^4.23.10", "@uiw/react-codemirror": "^4.23.10",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
@@ -62,11 +62,9 @@
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"fast-xml-parser": "^5.0.8", "fast-xml-parser": "^5.0.8",
"hono": "^4.7.11", "hono": "4.8.3",
"json-schema-form-react": "^0.0.2",
"json-schema-library": "10.0.0-rc7", "json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1", "json-schema-to-ts": "^3.1.1",
"jsonv-ts": "^0.1.0",
"kysely": "^0.27.6", "kysely": "^0.27.6",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1", "oauth4webapi": "^2.11.1",
@@ -80,7 +78,6 @@
"@cloudflare/vitest-pool-workers": "^0.8.38", "@cloudflare/vitest-pool-workers": "^0.8.38",
"@cloudflare/workers-types": "^4.20250606.0", "@cloudflare/workers-types": "^4.20250606.0",
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@hono/typebox-validator": "^0.3.3",
"@hono/vite-dev-server": "^0.19.1", "@hono/vite-dev-server": "^0.19.1",
"@hookform/resolvers": "^4.1.3", "@hookform/resolvers": "^4.1.3",
"@libsql/client": "^0.15.9", "@libsql/client": "^0.15.9",
@@ -88,6 +85,7 @@
"@mantine/notifications": "^7.17.1", "@mantine/notifications": "^7.17.1",
"@playwright/test": "^1.51.1", "@playwright/test": "^1.51.1",
"@rjsf/core": "5.22.2", "@rjsf/core": "5.22.2",
"@standard-schema/spec": "^1.0.0",
"@tabler/icons-react": "3.18.0", "@tabler/icons-react": "3.18.0",
"@tailwindcss/postcss": "^4.0.12", "@tailwindcss/postcss": "^4.0.12",
"@tailwindcss/vite": "^4.0.12", "@tailwindcss/vite": "^4.0.12",
@@ -103,6 +101,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"jotai": "^2.12.2", "jotai": "^2.12.2",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"jsonv-ts": "^0.3.2",
"kysely-d1": "^0.3.0", "kysely-d1": "^0.3.0",
"kysely-generic-sqlite": "^1.2.1", "kysely-generic-sqlite": "^1.2.1",
"libsql-stateless-easy": "^1.8.0", "libsql-stateless-easy": "^1.8.0",
@@ -127,6 +126,7 @@
"tsx": "^4.19.3", "tsx": "^4.19.3",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-plugin-circular-dependency": "^0.5.0",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9", "vitest": "^3.0.9",
"wouter": "^3.6.0" "wouter": "^3.6.0"
@@ -162,16 +162,6 @@
"import": "./dist/ui/client/index.js", "import": "./dist/ui/client/index.js",
"require": "./dist/ui/client/index.js" "require": "./dist/ui/client/index.js"
}, },
"./data": {
"types": "./dist/types/data/index.d.ts",
"import": "./dist/data/index.js",
"require": "./dist/data/index.js"
},
"./core": {
"types": "./dist/types/core/index.d.ts",
"import": "./dist/core/index.js",
"require": "./dist/core/index.js"
},
"./utils": { "./utils": {
"types": "./dist/types/core/utils/index.d.ts", "types": "./dist/types/core/utils/index.d.ts",
"import": "./dist/core/utils/index.js", "import": "./dist/core/utils/index.js",
@@ -182,11 +172,6 @@
"import": "./dist/cli/index.js", "import": "./dist/cli/index.js",
"require": "./dist/cli/index.js" "require": "./dist/cli/index.js"
}, },
"./media": {
"types": "./dist/types/media/index.d.ts",
"import": "./dist/media/index.js",
"require": "./dist/media/index.js"
},
"./plugins": { "./plugins": {
"types": "./dist/types/plugins/index.d.ts", "types": "./dist/types/plugins/index.d.ts",
"import": "./dist/plugins/index.js", "import": "./dist/plugins/index.js",
@@ -252,15 +237,13 @@
}, },
"./dist/main.css": "./dist/ui/main.css", "./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css", "./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json" "./dist/manifest.json": "./dist/static/.vite/manifest.json",
"./static/*": "./dist/static/*"
}, },
"typesVersions": { "typesVersions": {
"*": { "*": {
"data": ["./dist/types/data/index.d.ts"],
"core": ["./dist/types/core/index.d.ts"],
"utils": ["./dist/types/core/utils/index.d.ts"], "utils": ["./dist/types/core/utils/index.d.ts"],
"cli": ["./dist/types/cli/index.d.ts"], "cli": ["./dist/types/cli/index.d.ts"],
"media": ["./dist/types/media/index.d.ts"],
"plugins": ["./dist/types/plugins/index.d.ts"], "plugins": ["./dist/types/plugins/index.d.ts"],
"adapter": ["./dist/types/adapter/index.d.ts"], "adapter": ["./dist/types/adapter/index.d.ts"],
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"], "adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],

View File

@@ -1,4 +1,4 @@
import type { SafeUser } from "auth"; import type { SafeUser } from "bknd";
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi"; import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
import { DataApi, type DataApiOptions } from "data/api/DataApi"; import { DataApi, type DataApiOptions } from "data/api/DataApi";
import { decode } from "hono/jwt"; import { decode } from "hono/jwt";

View File

@@ -40,6 +40,9 @@ export class AppConfigUpdatedEvent extends AppEvent<{
}> { }> {
static override slug = "app-config-updated"; static override slug = "app-config-updated";
} }
/**
* @type {Event<{ app: App }>}
*/
export class AppBuiltEvent extends AppEvent { export class AppBuiltEvent extends AppEvent {
static override slug = "app-built"; static override slug = "app-built";
} }
@@ -71,6 +74,9 @@ export type AppOptions = {
}; };
}; };
export type CreateAppConfig = { export type CreateAppConfig = {
/**
* bla
*/
connection?: Connection | { url: string }; connection?: Connection | { url: string };
initialConfig?: InitialModuleConfigs; initialConfig?: InitialModuleConfigs;
options?: AppOptions; options?: AppOptions;

View File

@@ -3,10 +3,9 @@
import path from "node:path"; import path from "node:path";
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter"; import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import { registerLocalMediaAdapter } from "."; import { registerLocalMediaAdapter } from ".";
import { config } from "bknd/core"; import { config, type App } from "bknd";
import type { ServeOptions } from "bun"; import type { ServeOptions } from "bun";
import { serveStatic } from "hono/bun"; import { serveStatic } from "hono/bun";
import type { App } from "App";
type BunEnv = Bun.Env; type BunEnv = Bun.Env;
export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">; export type BunBkndConfig<Env = BunEnv> = RuntimeBkndConfig<Env> & Omit<ServeOptions, "fetch">;
@@ -21,8 +20,8 @@ export async function createApp<Env = BunEnv>(
return await createRuntimeApp( return await createRuntimeApp(
{ {
...config,
serveStatic: serveStatic({ root }), serveStatic: serveStatic({ root }),
...config,
}, },
args ?? (process.env as Env), args ?? (process.env as Env),
opts, opts,
@@ -53,6 +52,7 @@ export function serve<Env = BunEnv>(
onBuilt, onBuilt,
buildConfig, buildConfig,
adminOptions, adminOptions,
serveStatic,
...serveOptions ...serveOptions
}: BunBkndConfig<Env> = {}, }: BunBkndConfig<Env> = {},
args: Env = {} as Env, args: Env = {} as Env,
@@ -70,6 +70,7 @@ export function serve<Env = BunEnv>(
buildConfig, buildConfig,
adminOptions, adminOptions,
distPath, distPath,
serveStatic,
}, },
args, args,
opts, opts,

View File

@@ -1,5 +1,5 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { genericSqlite, type GenericSqliteConnection } from "bknd/data"; import { genericSqlite, type GenericSqliteConnection } from "bknd";
export type BunSqliteConnection = GenericSqliteConnection<Database>; export type BunSqliteConnection = GenericSqliteConnection<Database>;
export type BunSqliteConnectionConfig = { export type BunSqliteConnectionConfig = {

View File

@@ -12,7 +12,10 @@ export function getBindings<T extends GetBindingType>(env: any, type: T): Bindin
const bindings: BindingMap<T>[] = []; const bindings: BindingMap<T>[] = [];
for (const key in env) { for (const key in env) {
try { try {
if (env[key] && (env[key] as any).constructor.name === type) { if (
env[key] &&
((env[key] as any).constructor.name === type || String(env[key]) === `[object ${type}]`)
) {
bindings.push({ bindings.push({
key, key,
value: env[key] as BindingTypeMap[T], value: env[key] as BindingTypeMap[T],

View File

@@ -1,16 +1,16 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import { Connection } from "bknd";
import { sqlite } from "bknd/adapter/sqlite";
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
import { registerMedia } from "./storage/StorageR2Adapter"; import { registerMedia } from "./storage/StorageR2Adapter";
import { getBinding } from "./bindings"; import { getBinding } from "./bindings";
import { d1Sqlite } from "./connection/D1Connection"; import { d1Sqlite } from "./connection/D1Connection";
import { Connection } from "bknd/data";
import type { CloudflareBkndConfig, CloudflareEnv } from "."; import type { CloudflareBkndConfig, CloudflareEnv } from ".";
import { App } from "bknd"; import { App } from "bknd";
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
import type { Context, ExecutionContext } from "hono"; import type { Context, ExecutionContext } from "hono";
import { $console } from "core/utils"; import { $console } from "core/utils";
import { setCookie } from "hono/cookie"; import { setCookie } from "hono/cookie";
import { sqlite } from "bknd/adapter/sqlite";
export const constants = { export const constants = {
exec_async_event_id: "cf_register_waituntil", exec_async_event_id: "cf_register_waituntil",

View File

@@ -1,6 +1,6 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import { genericSqlite, type GenericSqliteConnection } from "bknd/data"; import { genericSqlite, type GenericSqliteConnection } from "bknd";
import type { QueryResult } from "kysely"; import type { QueryResult } from "kysely";
export type D1SqliteConnection = GenericSqliteConnection<D1Database>; export type D1SqliteConnection = GenericSqliteConnection<D1Database>;

View File

@@ -1,6 +1,6 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import { genericSqlite, type GenericSqliteConnection } from "bknd/data"; import { genericSqlite, type GenericSqliteConnection } from "bknd";
import type { QueryResult } from "kysely"; import type { QueryResult } from "kysely";
export type D1SqliteConnection = GenericSqliteConnection<D1Database>; export type D1SqliteConnection = GenericSqliteConnection<D1Database>;

View File

@@ -13,7 +13,7 @@ export {
type BindingMap, type BindingMap,
} from "./bindings"; } from "./bindings";
export { constants } from "./config"; export { constants } from "./config";
export { StorageR2Adapter } from "./storage/StorageR2Adapter"; export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
export { registries } from "bknd"; export { registries } from "bknd";
// for compatibility with old code // for compatibility with old code

View File

@@ -1,16 +1,12 @@
import { registries } from "bknd"; import { registries, isDebug, guessMimeType } from "bknd";
import { isDebug } from "bknd/core";
// @ts-ignore
import { StringEnum } from "bknd/utils";
import { guessMimeType as guess, StorageAdapter, type FileBody } from "bknd/media";
import { getBindings } from "../bindings"; import { getBindings } from "../bindings";
import * as tb from "@sinclair/typebox"; import { s } from "bknd/utils";
const { Type } = tb; import { StorageAdapter, type FileBody } from "bknd";
export function makeSchema(bindings: string[] = []) { export function makeSchema(bindings: string[] = []) {
return Type.Object( return s.object(
{ {
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String()), binding: bindings.length > 0 ? s.string({ enum: bindings }) : s.string().optional(),
}, },
{ title: "R2", description: "Cloudflare R2 storage" }, { title: "R2", description: "Cloudflare R2 storage" },
); );
@@ -93,7 +89,7 @@ export class StorageR2Adapter extends StorageAdapter {
const responseHeaders = new Headers({ const responseHeaders = new Headers({
"Accept-Ranges": "bytes", "Accept-Ranges": "bytes",
"Content-Type": guess(key), "Content-Type": guessMimeType(key),
}); });
const range = headers.has("range"); const range = headers.has("range");
@@ -145,7 +141,7 @@ export class StorageR2Adapter extends StorageAdapter {
if (!metadata || Object.keys(metadata).length === 0) { if (!metadata || Object.keys(metadata).length === 0) {
// guessing is especially required for dev environment (miniflare) // guessing is especially required for dev environment (miniflare)
metadata = { metadata = {
contentType: guess(object.key), contentType: guessMimeType(object.key),
}; };
} }
@@ -162,7 +158,7 @@ export class StorageR2Adapter extends StorageAdapter {
} }
return { return {
type: String(head.httpMetadata?.contentType ?? guess(key)), type: String(head.httpMetadata?.contentType ?? guessMimeType(key)),
size: head.size, size: head.size,
}; };
} }

View File

@@ -1,11 +1,8 @@
import { App, type CreateAppConfig } from "bknd"; import { config as $config, App, type CreateAppConfig, Connection, guessMimeType } from "bknd";
import { config as $config } from "bknd/core";
import { $console } from "bknd/utils"; import { $console } from "bknd/utils";
import type { MiddlewareHandler } from "hono"; import type { Context, MiddlewareHandler, Next } from "hono";
import type { AdminControllerOptions } from "modules/server/AdminController"; import type { AdminControllerOptions } from "modules/server/AdminController";
import { Connection } from "bknd/data"; import type { Manifest } from "vite";
export { Connection } from "bknd/data";
export type BkndConfig<Args = any> = CreateAppConfig & { export type BkndConfig<Args = any> = CreateAppConfig & {
app?: CreateAppConfig | ((args: Args) => CreateAppConfig); app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
@@ -72,7 +69,7 @@ export async function createAdapterApp<Config extends BkndConfig = BkndConfig, A
const sqlite = (await import("bknd/adapter/sqlite")).sqlite; const sqlite = (await import("bknd/adapter/sqlite")).sqlite;
const conf = appConfig.connection ?? { url: ":memory:" }; const conf = appConfig.connection ?? { url: ":memory:" };
connection = sqlite(conf); connection = sqlite(conf);
$console.info(`Using ${connection.name} connection`, conf.url); $console.info(`Using ${connection!.name} connection`, conf.url);
} }
appConfig.connection = connection; appConfig.connection = connection;
} }
@@ -140,3 +137,54 @@ export async function createRuntimeApp<Args = DefaultArgs>(
return app; return app;
} }
/**
* Creates a middleware handler to serve static assets via dynamic imports.
* This is useful for environments where filesystem access is limited but bundled assets can be imported.
*
* @param manifest - Vite manifest object containing asset information
* @returns Hono middleware handler for serving static assets
*
* @example
* ```typescript
* import { serveStaticViaImport } from "bknd/adapter";
*
* serve({
* serveStatic: serveStaticViaImport(),
* });
* ```
*/
export function serveStaticViaImport(opts?: { manifest?: Manifest }) {
let files: string[] | undefined;
// @ts-ignore
return async (c: Context, next: Next) => {
if (!files) {
const manifest =
opts?.manifest || ((await import("bknd/dist/manifest.json")).default as Manifest);
files = Object.values(manifest).flatMap((asset) => [asset.file, ...(asset.css || [])]);
}
const path = c.req.path.substring(1);
if (files.includes(path)) {
try {
const content = await import(/* @vite-ignore */ `bknd/static/${path}?raw`, {
assert: { type: "text" },
}).then((m) => m.default);
if (content) {
return c.body(content, {
headers: {
"Content-Type": guessMimeType(path),
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}
} catch (e) {
console.error("Error serving static file:", e);
return c.text("File not found", 404);
}
}
await next();
};
}

View File

@@ -1,4 +1,4 @@
import { genericSqlite } from "bknd/data"; import { genericSqlite } from "bknd";
import { DatabaseSync } from "node:sqlite"; import { DatabaseSync } from "node:sqlite";
export type NodeSqliteConnectionConfig = { export type NodeSqliteConnectionConfig = {

View File

@@ -3,9 +3,8 @@ import { serve as honoServe } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static"; import { serveStatic } from "@hono/node-server/serve-static";
import { registerLocalMediaAdapter } from "adapter/node/storage"; import { registerLocalMediaAdapter } from "adapter/node/storage";
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter"; import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import { config as $config } from "bknd/core"; import { config as $config, type App } from "bknd";
import { $console } from "core/utils"; import { $console } from "bknd/utils";
import type { App } from "App";
type NodeEnv = NodeJS.ProcessEnv; type NodeEnv = NodeJS.ProcessEnv;
export type NodeBkndConfig<Env = NodeEnv> = RuntimeBkndConfig<Env> & { export type NodeBkndConfig<Env = NodeEnv> = RuntimeBkndConfig<Env> & {
@@ -32,8 +31,8 @@ export async function createApp<Env = NodeEnv>(
registerLocalMediaAdapter(); registerLocalMediaAdapter();
return await createRuntimeApp( return await createRuntimeApp(
{ {
...config,
serveStatic: serveStatic({ root }), serveStatic: serveStatic({ root }),
...config,
}, },
// @ts-ignore // @ts-ignore
args ?? { env: process.env }, args ?? { env: process.env },

View File

@@ -1,17 +1,15 @@
import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises"; import { readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
import { type Static, isFile, parse } from "bknd/utils"; import type { FileBody, FileListObject, FileMeta, FileUploadPayload } from "bknd";
import type { FileBody, FileListObject, FileMeta, FileUploadPayload } from "bknd/media"; import { StorageAdapter, guessMimeType } from "bknd";
import { StorageAdapter, guessMimeType as guess } from "bknd/media"; import { parse, s, isFile } from "bknd/utils";
import * as tb from "@sinclair/typebox";
const { Type } = tb;
export const localAdapterConfig = Type.Object( export const localAdapterConfig = s.object(
{ {
path: Type.String({ default: "./" }), path: s.string({ default: "./" }),
}, },
{ title: "Local", description: "Local file system storage", additionalProperties: false }, { title: "Local", description: "Local file system storage", additionalProperties: false },
); );
export type LocalAdapterConfig = Static<typeof localAdapterConfig>; export type LocalAdapterConfig = s.Static<typeof localAdapterConfig>;
export class StorageLocalAdapter extends StorageAdapter { export class StorageLocalAdapter extends StorageAdapter {
private config: LocalAdapterConfig; private config: LocalAdapterConfig;
@@ -62,8 +60,7 @@ export class StorageLocalAdapter extends StorageAdapter {
} }
const filePath = `${this.config.path}/${key}`; const filePath = `${this.config.path}/${key}`;
const is_file = isFile(body); await writeFile(filePath, isFile(body) ? body.stream() : body);
await writeFile(filePath, is_file ? body.stream() : body);
return await this.computeEtag(body); return await this.computeEtag(body);
} }
@@ -86,7 +83,7 @@ export class StorageLocalAdapter extends StorageAdapter {
async getObject(key: string, headers: Headers): Promise<Response> { async getObject(key: string, headers: Headers): Promise<Response> {
try { try {
const content = await readFile(`${this.config.path}/${key}`); const content = await readFile(`${this.config.path}/${key}`);
const mimeType = guess(key); const mimeType = guessMimeType(key);
return new Response(content, { return new Response(content, {
status: 200, status: 200,
@@ -108,7 +105,7 @@ export class StorageLocalAdapter extends StorageAdapter {
async getObjectMeta(key: string): Promise<FileMeta> { async getObjectMeta(key: string): Promise<FileMeta> {
const stats = await stat(`${this.config.path}/${key}`); const stats = await stat(`${this.config.path}/${key}`);
return { return {
type: guess(key) || "application/octet-stream", type: guessMimeType(key) || "application/octet-stream",
size: stats.size, size: stats.size,
}; };
} }

View File

@@ -1,4 +1,4 @@
import type { Connection } from "bknd/data"; import type { Connection } from "bknd";
import { bunSqlite } from "../bun/connection/BunSqliteConnection"; import { bunSqlite } from "../bun/connection/BunSqliteConnection";
export function sqlite(config?: { url: string }): Connection { export function sqlite(config?: { url: string }): Connection {

View File

@@ -1,4 +1,4 @@
import { type Connection, libsql } from "bknd/data"; import { type Connection, libsql } from "bknd";
export function sqlite(config: { url: string }): Connection { export function sqlite(config: { url: string }): Connection {
return libsql(config); return libsql(config);

View File

@@ -1,4 +1,4 @@
import type { Connection } from "bknd/data"; import type { Connection } from "bknd";
import { nodeSqlite } from "../node/connection/NodeSqliteConnection"; import { nodeSqlite } from "../node/connection/NodeSqliteConnection";
export function sqlite(config?: { url: string }): Connection { export function sqlite(config?: { url: string }): Connection {

View File

@@ -1,8 +1,9 @@
import { Authenticator, AuthPermissions, Role, type Strategy } from "auth"; import type { DB } from "bknd";
import type { PasswordStrategy } from "auth/authenticate/strategies"; import * as AuthPermissions from "auth/auth-permissions";
import type { DB } from "core"; import type { AuthStrategy } from "auth/authenticate/strategies/Strategy";
import type { PasswordStrategy } from "auth/authenticate/strategies/PasswordStrategy";
import { $console, secureRandomString, transformObject } from "core/utils"; import { $console, secureRandomString, transformObject } from "core/utils";
import type { Entity, EntityManager } from "data"; import type { Entity, EntityManager } from "data/entities";
import { em, entity, enumm, type FieldSchema } from "data/prototype"; import { em, entity, enumm, type FieldSchema } from "data/prototype";
import { Module } from "modules/Module"; import { Module } from "modules/Module";
import { AuthController } from "./api/AuthController"; import { AuthController } from "./api/AuthController";
@@ -10,9 +11,11 @@ import { type AppAuthSchema, authConfigSchema, STRATEGIES } from "./auth-schema"
import { AppUserPool } from "auth/AppUserPool"; import { AppUserPool } from "auth/AppUserPool";
import type { AppEntity } from "core/config"; import type { AppEntity } from "core/config";
import { usersFields } from "./auth-entities"; import { usersFields } from "./auth-entities";
import { Authenticator } from "./authenticate/Authenticator";
import { Role } from "./authorize/Role";
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>; export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
declare module "core" { declare module "bknd" {
interface Users extends AppEntity, UserFieldSchema {} interface Users extends AppEntity, UserFieldSchema {}
interface DB { interface DB {
users: Users; users: Users;
@@ -21,7 +24,7 @@ declare module "core" {
export type CreateUserPayload = { email: string; password: string; [key: string]: any }; export type CreateUserPayload = { email: string; password: string; [key: string]: any };
export class AppAuth extends Module<typeof authConfigSchema> { export class AppAuth extends Module<AppAuthSchema> {
private _authenticator?: Authenticator; private _authenticator?: Authenticator;
cache: Record<string, any> = {}; cache: Record<string, any> = {};
_controller!: AuthController; _controller!: AuthController;
@@ -88,7 +91,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
this.ctx.guard.registerPermissions(AuthPermissions); this.ctx.guard.registerPermissions(AuthPermissions);
} }
isStrategyEnabled(strategy: Strategy | string) { isStrategyEnabled(strategy: AuthStrategy | string) {
const name = typeof strategy === "string" ? strategy : strategy.getName(); const name = typeof strategy === "string" ? strategy : strategy.getName();
// for now, password is always active // for now, password is always active
if (name === "password") return true; if (name === "password") return true;
@@ -187,6 +190,6 @@ export class AppAuth extends Module<typeof authConfigSchema> {
enabled: this.isStrategyEnabled(strategy), enabled: this.isStrategyEnabled(strategy),
...strategy.toJSON(secrets), ...strategy.toJSON(secrets),
})), })),
}; } as AppAuthSchema;
} }
} }

View File

@@ -1,6 +1,6 @@
import type { AuthActionResponse } from "auth/api/AuthController"; import type { AuthActionResponse } from "auth/api/AuthController";
import type { AppAuthSchema } from "auth/auth-schema"; import type { AppAuthSchema } from "auth/auth-schema";
import type { AuthResponse, SafeUser, Strategy } from "auth/authenticate/Authenticator"; import type { AuthResponse, SafeUser, AuthStrategy } from "bknd";
import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi"; import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi";
export type AuthApiOptions = BaseModuleApiOptions & { export type AuthApiOptions = BaseModuleApiOptions & {
@@ -39,7 +39,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
} }
async actionSchema(strategy: string, action: string) { async actionSchema(strategy: string, action: string) {
return this.get<Strategy>([strategy, "actions", action, "schema.json"]); return this.get<AuthStrategy>([strategy, "actions", action, "schema.json"]);
} }
async action(strategy: string, action: string, input: any) { async action(strategy: string, action: string, input: any) {

View File

@@ -1,9 +1,11 @@
import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth"; import type { SafeUser } from "bknd";
import { TypeInvalidError, parse, transformObject } from "core/utils"; import type { AuthStrategy } from "auth/authenticate/strategies/Strategy";
import { DataPermissions } from "data"; import type { AppAuth } from "auth/AppAuth";
import * as AuthPermissions from "auth/auth-permissions";
import * as DataPermissions from "data/permissions";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { Controller, type ServerEnv } from "modules/Controller"; import { Controller, type ServerEnv } from "modules/Controller";
import { describeRoute, jsc, s } from "core/object/schema"; import { describeRoute, jsc, s, parse, InvalidSchemaError, transformObject } from "bknd/utils";
export type AuthActionResponse = { export type AuthActionResponse = {
success: boolean; success: boolean;
@@ -30,7 +32,7 @@ export class AuthController extends Controller {
return this.em.repo(entity_name as "users"); return this.em.repo(entity_name as "users");
} }
private registerStrategyActions(strategy: Strategy, mainHono: Hono<ServerEnv>) { private registerStrategyActions(strategy: AuthStrategy, mainHono: Hono<ServerEnv>) {
if (!this.auth.isStrategyEnabled(strategy)) { if (!this.auth.isStrategyEnabled(strategy)) {
return; return;
} }
@@ -58,7 +60,7 @@ export class AuthController extends Controller {
try { try {
const body = await this.auth.authenticator.getBody(c); const body = await this.auth.authenticator.getBody(c);
const valid = parse(create.schema, body, { const valid = parse(create.schema, body, {
skipMark: true, //skipMark: true,
}); });
const processed = (await create.preprocess?.(valid)) ?? valid; const processed = (await create.preprocess?.(valid)) ?? valid;
@@ -78,7 +80,7 @@ export class AuthController extends Controller {
data: created as unknown as SafeUser, data: created as unknown as SafeUser,
} as AuthActionResponse); } as AuthActionResponse);
} catch (e) { } catch (e) {
if (e instanceof TypeInvalidError) { if (e instanceof InvalidSchemaError) {
return c.json( return c.json(
{ {
success: false, success: false,

View File

@@ -1,4 +1,4 @@
import { Permission } from "core"; import { Permission } from "core/security/Permission";
export const createUser = new Permission("auth.user.create"); export const createUser = new Permission("auth.user.create");
//export const updateUser = new Permission("auth.user.update"); //export const updateUser = new Permission("auth.user.update");

View File

@@ -1,8 +1,6 @@
import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator"; import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator";
import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies"; import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies";
import { type Static, StringRecord, objectTransform } from "core/utils"; import { objectTransform, s } from "bknd/utils";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
export const Strategies = { export const Strategies = {
password: { password: {
@@ -21,64 +19,58 @@ export const Strategies = {
export const STRATEGIES = Strategies; export const STRATEGIES = Strategies;
const strategiesSchemaObject = objectTransform(STRATEGIES, (strategy, name) => { const strategiesSchemaObject = objectTransform(STRATEGIES, (strategy, name) => {
return Type.Object( return s.strictObject(
{ {
enabled: Type.Optional(Type.Boolean({ default: true })), enabled: s.boolean({ default: true }).optional(),
type: Type.Const(name, { default: name, readOnly: true }), type: s.literal(name),
config: strategy.schema, config: strategy.schema,
}, },
{ {
title: name, title: name,
additionalProperties: false,
}, },
); );
}); });
const strategiesSchema = Type.Union(Object.values(strategiesSchemaObject));
export type AppAuthStrategies = Static<typeof strategiesSchema>;
export type AppAuthOAuthStrategy = Static<typeof STRATEGIES.oauth.schema>;
export type AppAuthCustomOAuthStrategy = Static<typeof STRATEGIES.custom_oauth.schema>;
const guardConfigSchema = Type.Object({ const strategiesSchema = s.anyOf(Object.values(strategiesSchemaObject));
enabled: Type.Optional(Type.Boolean({ default: false })), export type AppAuthStrategies = s.Static<typeof strategiesSchema>;
export type AppAuthOAuthStrategy = s.Static<typeof STRATEGIES.oauth.schema>;
export type AppAuthCustomOAuthStrategy = s.Static<typeof STRATEGIES.custom_oauth.schema>;
const guardConfigSchema = s.object({
enabled: s.boolean({ default: false }).optional(),
});
export const guardRoleSchema = s.strictObject({
permissions: s.array(s.string()).optional(),
is_default: s.boolean().optional(),
implicit_allow: s.boolean().optional(),
}); });
export const guardRoleSchema = Type.Object(
{
permissions: Type.Optional(Type.Array(Type.String())),
is_default: Type.Optional(Type.Boolean()),
implicit_allow: Type.Optional(Type.Boolean()),
},
{ additionalProperties: false },
);
export const authConfigSchema = Type.Object( export const authConfigSchema = s.strictObject(
{ {
enabled: Type.Boolean({ default: false }), enabled: s.boolean({ default: false }),
basepath: Type.String({ default: "/api/auth" }), basepath: s.string({ default: "/api/auth" }),
entity_name: Type.String({ default: "users" }), entity_name: s.string({ default: "users" }),
allow_register: Type.Optional(Type.Boolean({ default: true })), allow_register: s.boolean({ default: true }).optional(),
jwt: jwtConfig, jwt: jwtConfig,
cookie: cookieConfig, cookie: cookieConfig,
strategies: Type.Optional( strategies: s.record(strategiesSchema, {
StringRecord(strategiesSchema, { title: "Strategies",
title: "Strategies", default: {
default: { password: {
password: { type: "password",
type: "password", enabled: true,
enabled: true, config: {
config: { hashing: "sha256",
hashing: "sha256",
},
}, },
}, },
}), },
), }),
guard: Type.Optional(guardConfigSchema), guard: guardConfigSchema.optional(),
roles: Type.Optional(StringRecord(guardRoleSchema, { default: {} })), roles: s.record(guardRoleSchema, { default: {} }).optional(),
},
{
title: "Authentication",
additionalProperties: false,
}, },
{ title: "Authentication" },
); );
export type AppAuthSchema = Static<typeof authConfigSchema>; export type AppAuthJWTConfig = s.Static<typeof jwtConfig>;
export type AppAuthSchema = s.Static<typeof authConfigSchema>;

View File

@@ -1,46 +1,27 @@
import { type DB, Exception } from "core"; import type { DB } from "bknd";
import { Exception } from "core/errors";
import { addFlashMessage } from "core/server/flash"; import { addFlashMessage } from "core/server/flash";
import { import type { Context } from "hono";
$console,
type Static,
StringEnum,
type TObject,
parse,
runtimeSupports,
truncate,
} from "core/utils";
import type { Context, Hono } from "hono";
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
import { sign, verify } from "hono/jwt"; import { sign, verify } from "hono/jwt";
import type { CookieOptions } from "hono/utils/cookie"; import { type CookieOptions, serializeSigned } from "hono/utils/cookie";
import type { ServerEnv } from "modules/Controller"; import type { ServerEnv } from "modules/Controller";
import { pick } from "lodash-es"; import { pick } from "lodash-es";
import * as tbbox from "@sinclair/typebox";
import { InvalidConditionsException } from "auth/errors"; import { InvalidConditionsException } from "auth/errors";
const { Type } = tbbox; import { s, parse, secret, runtimeSupports, truncate, $console } from "bknd/utils";
import type { AuthStrategy } from "./strategies/Strategy";
type Input = any; // workaround type Input = any; // workaround
export type JWTPayload = Parameters<typeof sign>[0]; export type JWTPayload = Parameters<typeof sign>[0];
export const strategyActions = ["create", "change"] as const; export const strategyActions = ["create", "change"] as const;
export type StrategyActionName = (typeof strategyActions)[number]; export type StrategyActionName = (typeof strategyActions)[number];
export type StrategyAction<S extends TObject = TObject> = { export type StrategyAction<S extends s.ObjectSchema = s.ObjectSchema> = {
schema: S; schema: S;
preprocess: (input: Static<S>) => Promise<Omit<DB["users"], "id" | "strategy">>; preprocess: (input: s.Static<S>) => Promise<Omit<DB["users"], "id" | "strategy">>;
}; };
export type StrategyActions = Partial<Record<StrategyActionName, StrategyAction>>; export type StrategyActions = Partial<Record<StrategyActionName, StrategyAction>>;
// @todo: add schema to interface to ensure proper inference
// @todo: add tests (e.g. invalid strategy_value)
export interface Strategy {
getController: (auth: Authenticator) => Hono<any>;
getType: () => string;
getMode: () => "form" | "external";
getName: () => string;
toJSON: (secrets?: boolean) => any;
getActions?: () => StrategyActions;
}
export type User = DB["users"]; export type User = DB["users"];
export type ProfileExchange = { export type ProfileExchange = {
@@ -60,43 +41,45 @@ export interface UserPool {
} }
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
export const cookieConfig = Type.Partial( export const cookieConfig = s
Type.Object({ .object({
path: Type.String({ default: "/" }), path: s.string({ default: "/" }),
sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }), sameSite: s.string({ enum: ["strict", "lax", "none"], default: "lax" }),
secure: Type.Boolean({ default: true }), secure: s.boolean({ default: true }),
httpOnly: Type.Boolean({ default: true }), httpOnly: s.boolean({ default: true }),
expires: Type.Number({ default: defaultCookieExpires }), // seconds expires: s.number({ default: defaultCookieExpires }), // seconds
renew: Type.Boolean({ default: true }), partitioned: s.boolean({ default: false }),
pathSuccess: Type.String({ default: "/" }), renew: s.boolean({ default: true }),
pathLoggedOut: Type.String({ default: "/" }), pathSuccess: s.string({ default: "/" }),
}), pathLoggedOut: s.string({ default: "/" }),
{ default: {}, additionalProperties: false }, })
); .partial()
.strict();
// @todo: maybe add a config to not allow cookie/api tokens to be used interchangably? // @todo: maybe add a config to not allow cookie/api tokens to be used interchangably?
// see auth.integration test for further details // see auth.integration test for further details
export const jwtConfig = Type.Object( export const jwtConfig = s
{ .object(
// @todo: autogenerate a secret if not present. But it must be persisted from AppAuth {
secret: Type.String({ default: "" }), // @todo: autogenerate a secret if not present. But it must be persisted from AppAuth
alg: Type.Optional(StringEnum(["HS256", "HS384", "HS512"], { default: "HS256" })), secret: secret({ default: "" }),
expires: Type.Optional(Type.Number()), // seconds alg: s.string({ enum: ["HS256", "HS384", "HS512"], default: "HS256" }).optional(),
issuer: Type.Optional(Type.String()), expires: s.number().optional(), // seconds
fields: Type.Array(Type.String(), { default: ["id", "email", "role"] }), issuer: s.string().optional(),
}, fields: s.array(s.string(), { default: ["id", "email", "role"] }),
{ },
default: {}, {
additionalProperties: false, default: {},
}, },
); )
export const authenticatorConfig = Type.Object({ .strict();
export const authenticatorConfig = s.object({
jwt: jwtConfig, jwt: jwtConfig,
cookie: cookieConfig, cookie: cookieConfig,
}); });
type AuthConfig = Static<typeof authenticatorConfig>; type AuthConfig = s.Static<typeof authenticatorConfig>;
export type AuthAction = "login" | "register"; export type AuthAction = "login" | "register";
export type AuthResolveOptions = { export type AuthResolveOptions = {
identifier?: "email" | string; identifier?: "email" | string;
@@ -105,7 +88,7 @@ export type AuthResolveOptions = {
}; };
export type AuthUserResolver = ( export type AuthUserResolver = (
action: AuthAction, action: AuthAction,
strategy: Strategy, strategy: AuthStrategy,
profile: ProfileExchange, profile: ProfileExchange,
opts?: AuthResolveOptions, opts?: AuthResolveOptions,
) => Promise<ProfileExchange | undefined>; ) => Promise<ProfileExchange | undefined>;
@@ -115,7 +98,9 @@ type AuthClaims = SafeUser & {
exp?: number; exp?: number;
}; };
export class Authenticator<Strategies extends Record<string, Strategy> = Record<string, Strategy>> { export class Authenticator<
Strategies extends Record<string, AuthStrategy> = Record<string, AuthStrategy>,
> {
private readonly config: AuthConfig; private readonly config: AuthConfig;
constructor( constructor(
@@ -128,7 +113,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
async resolveLogin( async resolveLogin(
c: Context, c: Context,
strategy: Strategy, strategy: AuthStrategy,
profile: Partial<SafeUser>, profile: Partial<SafeUser>,
verify: (user: User) => Promise<void>, verify: (user: User) => Promise<void>,
opts?: AuthResolveOptions, opts?: AuthResolveOptions,
@@ -166,7 +151,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
async resolveRegister( async resolveRegister(
c: Context, c: Context,
strategy: Strategy, strategy: AuthStrategy,
profile: CreateUser, profile: CreateUser,
verify: (user: User) => Promise<void>, verify: (user: User) => Promise<void>,
opts?: AuthResolveOptions, opts?: AuthResolveOptions,
@@ -235,7 +220,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
strategy< strategy<
StrategyName extends keyof Strategies, StrategyName extends keyof Strategies,
Strat extends Strategy = Strategies[StrategyName], Strat extends AuthStrategy = Strategies[StrategyName],
>(strategy: StrategyName): Strat { >(strategy: StrategyName): Strat {
try { try {
return this.strategies[strategy] as unknown as Strat; return this.strategies[strategy] as unknown as Strat;
@@ -342,6 +327,11 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
await setSignedCookie(c, "auth", token, secret, this.cookieOptions); await setSignedCookie(c, "auth", token, secret, this.cookieOptions);
} }
async unsafeGetAuthCookie(token: string): Promise<string | undefined> {
// this works for as long as cookieOptions.prefix is not set
return serializeSigned("auth", token, this.config.jwt.secret, this.cookieOptions);
}
private deleteAuthCookie(c: Context) { private deleteAuthCookie(c: Context) {
$console.debug("deleting auth cookie"); $console.debug("deleting auth cookie");
deleteCookie(c, "auth", this.cookieOptions); deleteCookie(c, "auth", this.cookieOptions);

View File

@@ -1,21 +1,22 @@
import { type Authenticator, InvalidCredentialsException, type User } from "auth"; import type { User } from "bknd";
import { tbValidator as tb } from "core"; import type { Authenticator } from "auth/authenticate/Authenticator";
import { $console, hash, parse, type Static, StrictObject, StringEnum } from "core/utils"; import { InvalidCredentialsException } from "auth/errors";
import { hash, $console } from "core/utils";
import { Hono } from "hono"; import { Hono } from "hono";
import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs"; import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs";
import * as tbbox from "@sinclair/typebox"; import { AuthStrategy } from "./Strategy";
import { Strategy } from "./Strategy"; import { s, parse, jsc } from "bknd/utils";
const { Type } = tbbox; const schema = s
.object({
hashing: s.string({ enum: ["plain", "sha256", "bcrypt"], default: "sha256" }),
rounds: s.number({ minimum: 1, maximum: 10 }).optional(),
})
.strict();
const schema = StrictObject({ export type PasswordStrategyOptions = s.Static<typeof schema>;
hashing: StringEnum(["plain", "sha256", "bcrypt"], { default: "sha256" }),
rounds: Type.Optional(Type.Number({ minimum: 1, maximum: 10 })),
});
export type PasswordStrategyOptions = Static<typeof schema>; export class PasswordStrategy extends AuthStrategy<typeof schema> {
export class PasswordStrategy extends Strategy<typeof schema> {
constructor(config: Partial<PasswordStrategyOptions> = {}) { constructor(config: Partial<PasswordStrategyOptions> = {}) {
super(config as any, "password", "password", "form"); super(config as any, "password", "password", "form");
@@ -32,11 +33,11 @@ export class PasswordStrategy extends Strategy<typeof schema> {
} }
private getPayloadSchema() { private getPayloadSchema() {
return Type.Object({ return s.object({
email: Type.String({ email: s.string({
pattern: "^[\\w-\\.\\+_]+@([\\w-]+\\.)+[\\w-]{2,4}$", format: "email",
}), }),
password: Type.String({ password: s.string({
minLength: 8, // @todo: this should be configurable minLength: 8, // @todo: this should be configurable
}), }),
}); });
@@ -79,12 +80,12 @@ export class PasswordStrategy extends Strategy<typeof schema> {
getController(authenticator: Authenticator): Hono<any> { getController(authenticator: Authenticator): Hono<any> {
const hono = new Hono(); const hono = new Hono();
const redirectQuerySchema = Type.Object({ const redirectQuerySchema = s.object({
redirect: Type.Optional(Type.String()), redirect: s.string().optional(),
}); });
const payloadSchema = this.getPayloadSchema(); const payloadSchema = this.getPayloadSchema();
hono.post("/login", tb("query", redirectQuerySchema), async (c) => { hono.post("/login", jsc("query", redirectQuerySchema), async (c) => {
try { try {
const body = parse(payloadSchema, await authenticator.getBody(c), { const body = parse(payloadSchema, await authenticator.getBody(c), {
onError: (errors) => { onError: (errors) => {
@@ -102,7 +103,7 @@ export class PasswordStrategy extends Strategy<typeof schema> {
} }
}); });
hono.post("/register", tb("query", redirectQuerySchema), async (c) => { hono.post("/register", jsc("query", redirectQuerySchema), async (c) => {
try { try {
const { redirect } = c.req.valid("query"); const { redirect } = c.req.valid("query");
const { password, email, ...body } = parse( const { password, email, ...body } = parse(

View File

@@ -5,31 +5,31 @@ import type {
StrategyActions, StrategyActions,
} from "../Authenticator"; } from "../Authenticator";
import type { Hono } from "hono"; import type { Hono } from "hono";
import type { Static, TSchema } from "@sinclair/typebox"; import { type s, parse } from "bknd/utils";
import { parse, type TObject } from "core/utils";
export type StrategyMode = "form" | "external"; export type StrategyMode = "form" | "external";
export abstract class Strategy<Schema extends TSchema = TSchema> { export abstract class AuthStrategy<Schema extends s.Schema = s.Schema> {
protected actions: StrategyActions = {}; protected actions: StrategyActions = {};
constructor( constructor(
protected config: Static<Schema>, protected config: s.Static<Schema>,
public type: string, public type: string,
public name: string, public name: string,
public mode: StrategyMode, public mode: StrategyMode,
) { ) {
// don't worry about typing, it'll throw if invalid // don't worry about typing, it'll throw if invalid
this.config = parse(this.getSchema(), (config ?? {}) as any) as Static<Schema>; this.config = parse(this.getSchema(), (config ?? {}) as any) as s.Static<Schema>;
} }
protected registerAction<S extends TObject = TObject>( protected registerAction<S extends s.ObjectSchema = s.ObjectSchema>(
name: StrategyActionName, name: StrategyActionName,
schema: S, schema: S,
preprocess: StrategyAction<S>["preprocess"], preprocess: StrategyAction<S>["preprocess"],
): void { ): void {
this.actions[name] = { this.actions[name] = {
schema, schema,
// @ts-expect-error - @todo: fix this
preprocess, preprocess,
} as const; } as const;
} }
@@ -50,7 +50,7 @@ export abstract class Strategy<Schema extends TSchema = TSchema> {
return this.name; return this.name;
} }
toJSON(secrets?: boolean): { type: string; config: Static<Schema> | {} | undefined } { toJSON(secrets?: boolean): { type: string; config: s.Static<Schema> | {} | undefined } {
return { return {
type: this.getType(), type: this.getType(),
config: secrets ? this.config : undefined, config: secrets ? this.config : undefined,

View File

@@ -1,38 +1,36 @@
import { type Static, StrictObject, StringEnum } from "core/utils";
import * as tbbox from "@sinclair/typebox";
import type * as oauth from "oauth4webapi"; import type * as oauth from "oauth4webapi";
import { OAuthStrategy } from "./OAuthStrategy"; import { OAuthStrategy } from "./OAuthStrategy";
const { Type } = tbbox; import { s } from "bknd/utils";
type SupportedTypes = "oauth2" | "oidc"; type SupportedTypes = "oauth2" | "oidc";
type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>; type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
const UrlString = Type.String({ pattern: "^(https?|wss?)://[^\\s/$.?#].[^\\s]*$" }); const UrlString = s.string({ pattern: "^(https?|wss?)://[^\\s/$.?#].[^\\s]*$" });
const oauthSchemaCustom = StrictObject( const oauthSchemaCustom = s.strictObject(
{ {
type: StringEnum(["oidc", "oauth2"] as const, { default: "oidc" }), type: s.string({ enum: ["oidc", "oauth2"] as const, default: "oidc" }),
name: Type.String(), name: s.string(),
client: StrictObject({ client: s.object({
client_id: Type.String(), client_id: s.string(),
client_secret: Type.String(), client_secret: s.string(),
token_endpoint_auth_method: StringEnum(["client_secret_basic"]), token_endpoint_auth_method: s.string({ enum: ["client_secret_basic"] }),
}), }),
as: StrictObject({ as: s.strictObject({
issuer: Type.String(), issuer: s.string(),
code_challenge_methods_supported: Type.Optional(StringEnum(["S256"])), code_challenge_methods_supported: s.string({ enum: ["S256"] }).optional(),
scopes_supported: Type.Optional(Type.Array(Type.String())), scopes_supported: s.array(s.string()).optional(),
scope_separator: Type.Optional(Type.String({ default: " " })), scope_separator: s.string({ default: " " }).optional(),
authorization_endpoint: Type.Optional(UrlString), authorization_endpoint: UrlString.optional(),
token_endpoint: Type.Optional(UrlString), token_endpoint: UrlString.optional(),
userinfo_endpoint: Type.Optional(UrlString), userinfo_endpoint: UrlString.optional(),
}), }),
// @todo: profile mapping // @todo: profile mapping
}, },
{ title: "Custom OAuth" }, { title: "Custom OAuth" },
); );
type OAuthConfigCustom = Static<typeof oauthSchemaCustom>; type OAuthConfigCustom = s.Static<typeof oauthSchemaCustom>;
export type UserProfile = { export type UserProfile = {
sub: string; sub: string;

View File

@@ -1,31 +1,32 @@
import type { AuthAction, Authenticator } from "auth"; import type { Authenticator, AuthAction } from "auth/authenticate/Authenticator";
import { Exception, isDebug } from "core";
import { type Static, StringEnum, filterKeys, StrictObject } from "core/utils";
import { type Context, Hono } from "hono"; import { type Context, Hono } from "hono";
import { getSignedCookie, setSignedCookie } from "hono/cookie"; import { getSignedCookie, setSignedCookie } from "hono/cookie";
import * as oauth from "oauth4webapi"; import * as oauth from "oauth4webapi";
import * as issuers from "./issuers"; import * as issuers from "./issuers";
import * as tbbox from "@sinclair/typebox"; import { s, filterKeys } from "bknd/utils";
import { Strategy } from "auth/authenticate/strategies/Strategy"; import { Exception } from "core/errors";
const { Type } = tbbox; import { isDebug } from "core/env";
import { AuthStrategy } from "../Strategy";
type ConfiguredIssuers = keyof typeof issuers; type ConfiguredIssuers = keyof typeof issuers;
type SupportedTypes = "oauth2" | "oidc"; type SupportedTypes = "oauth2" | "oidc";
type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>; type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
const schemaProvided = Type.Object( const schemaProvided = s.object(
{ {
name: StringEnum(Object.keys(issuers) as ConfiguredIssuers[]), name: s.string({ enum: Object.keys(issuers) as ConfiguredIssuers[] }),
type: StringEnum(["oidc", "oauth2"] as const, { default: "oauth2" }), type: s.string({ enum: ["oidc", "oauth2"] as const, default: "oauth2" }),
client: StrictObject({ client: s
client_id: Type.String(), .object({
client_secret: Type.String(), client_id: s.string(),
}), client_secret: s.string(),
})
.strict(),
}, },
{ title: "OAuth" }, { title: "OAuth" },
); );
type ProvidedOAuthConfig = Static<typeof schemaProvided>; type ProvidedOAuthConfig = s.Static<typeof schemaProvided>;
export type CustomOAuthConfig = { export type CustomOAuthConfig = {
type: SupportedTypes; type: SupportedTypes;
@@ -69,7 +70,7 @@ export class OAuthCallbackException extends Exception {
} }
} }
export class OAuthStrategy extends Strategy<typeof schemaProvided> { export class OAuthStrategy extends AuthStrategy<typeof schemaProvided> {
constructor(config: ProvidedOAuthConfig) { constructor(config: ProvidedOAuthConfig) {
super(config, "oauth", config.name, "external"); super(config, "oauth", config.name, "external");
} }

View File

@@ -1,5 +1,6 @@
import { Exception, Permission } from "core"; import { Exception } from "core/errors";
import { $console, objectTransform } from "core/utils"; import { $console, objectTransform } from "core/utils";
import { Permission } from "core/security/Permission";
import type { Context } from "hono"; import type { Context } from "hono";
import type { ServerEnv } from "modules/Controller"; import type { ServerEnv } from "modules/Controller";
import { Role } from "./Role"; import { Role } from "./Role";

View File

@@ -1,4 +1,4 @@
import { Permission } from "core"; import { Permission } from "core/security/Permission";
export class RolePermission { export class RolePermission {
constructor( constructor(

View File

@@ -1,5 +1,6 @@
import { Exception, isDebug } from "core"; import { Exception } from "core/errors";
import { HttpStatus } from "core/utils"; import { isDebug } from "core/env";
import { HttpStatus } from "bknd/utils";
export class AuthException extends Exception { export class AuthException extends Exception {
getSafeErrorAndCode() { getSafeErrorAndCode() {

View File

@@ -1,22 +0,0 @@
export { UserExistsException, UserNotFoundException, InvalidCredentialsException } from "./errors";
export {
type ProfileExchange,
type Strategy,
type User,
type SafeUser,
type CreateUser,
type AuthResponse,
type UserPool,
type AuthAction,
type AuthUserResolver,
Authenticator,
authenticatorConfig,
jwtConfig,
} from "./authenticate/Authenticator";
export { AppAuth, type UserFieldSchema } from "./AppAuth";
export { Guard, type GuardUserContext, type GuardConfig } from "./authorize/Guard";
export { Role } from "./authorize/Role";
export * as AuthPermissions from "./auth-permissions";

View File

@@ -1,5 +1,5 @@
import type { Permission } from "core"; import type { Permission } from "core/security/Permission";
import { $console, patternMatch } from "core/utils"; import { $console, patternMatch } from "bknd/utils";
import type { Context } from "hono"; import type { Context } from "hono";
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import type { ServerEnv } from "modules/Controller"; import type { ServerEnv } from "modules/Controller";

View File

@@ -5,7 +5,7 @@ import type { CliCommand } from "cli/types";
import { typewriter, wait } from "cli/utils/cli"; import { typewriter, wait } from "cli/utils/cli";
import { execAsync, getVersion } from "cli/utils/sys"; import { execAsync, getVersion } from "cli/utils/sys";
import { Option } from "commander"; import { Option } from "commander";
import { env } from "core"; import { env } from "bknd";
import color from "picocolors"; import color from "picocolors";
import { overridePackageJson, updateBkndPackages } from "./npm"; import { overridePackageJson, updateBkndPackages } from "./npm";
import { type Template, templates, type TemplateSetupCtx } from "./templates"; import { type Template, templates, type TemplateSetupCtx } from "./templates";

View File

@@ -1,9 +1,8 @@
import type { Config } from "@libsql/client/node"; import type { Config } from "@libsql/client/node";
import type { App, CreateAppConfig } from "App";
import { StorageLocalAdapter } from "adapter/node/storage"; import { StorageLocalAdapter } from "adapter/node/storage";
import type { CliBkndConfig, CliCommand } from "cli/types"; import type { CliBkndConfig, CliCommand } from "cli/types";
import { Option } from "commander"; import { Option } from "commander";
import { config } from "core"; import { config, type App, type CreateAppConfig } from "bknd";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { registries } from "modules/registries"; import { registries } from "modules/registries";
import c from "picocolors"; import c from "picocolors";
@@ -16,8 +15,8 @@ import {
serveStatic, serveStatic,
startServer, startServer,
} from "./platform"; } from "./platform";
import { createRuntimeApp, makeConfig } from "adapter"; import { createRuntimeApp, makeConfig } from "bknd/adapter";
import { colorizeConsole, isBun } from "core/utils"; import { colorizeConsole, isBun } from "bknd/utils";
const env_files = [".env", ".dev.vars"]; const env_files = [".env", ".dev.vars"];
dotenv.config({ dotenv.config({

View File

@@ -1,7 +1,7 @@
import { PostHog } from "posthog-js-lite"; import { PostHog } from "posthog-js-lite";
import { getVersion } from "cli/utils/sys"; import { getVersion } from "cli/utils/sys";
import { env, isDebug } from "core"; import { env, isDebug } from "bknd";
import { $console } from "core/utils"; import { $console } from "bknd/utils";
type Properties = { [p: string]: any }; type Properties = { [p: string]: any };

View File

@@ -6,23 +6,3 @@ export interface IEmailDriver<Data = unknown, Options = object> {
options?: Options, options?: Options,
): Promise<Data>; ): Promise<Data>;
} }
import type { BkndConfig } from "bknd";
import { resendEmail, memoryCache } from "bknd/core";
export default {
onBuilt: async (app) => {
app.server.get("/send-email", async (c) => {
if (await app.drivers?.email?.send("test@test.com", "Test", "Test")) {
return c.text("success");
}
return c.text("failed");
});
},
options: {
drivers: {
email: resendEmail({ apiKey: "..." }),
cache: memoryCache(),
},
},
} as const satisfies BkndConfig;

View File

@@ -1,48 +0,0 @@
import type { Hono, MiddlewareHandler } from "hono";
export { tbValidator } from "./server/lib/tbValidator";
export { Exception, BkndError } from "./errors";
export { isDebug, env } from "./env";
export { type PrimaryFieldType, config, type DB, type AppEntity } from "./config";
export { AwsClient } from "./clients/aws/AwsClient";
export {
SimpleRenderer,
type TemplateObject,
type TemplateTypes,
type SimpleRendererOptions,
} from "./template/SimpleRenderer";
export { SchemaObject } from "./object/SchemaObject";
export { DebugLogger } from "./utils/DebugLogger";
export { Permission } from "./security/Permission";
export {
exp,
makeValidator,
type FilterQuery,
type Primitive,
isPrimitive,
type TExpression,
type BooleanLike,
isBooleanLike,
} from "./object/query/query";
export { Registry, type Constructor } from "./registry/Registry";
export { getFlashMessage } from "./server/flash";
export {
s,
parse,
jsc,
describeRoute,
schemaToSpec,
openAPISpecs,
type ParseOptions,
InvalidSchemaError,
} from "./object/schema";
export * from "./drivers";
export * from "./events";
// compatibility
export type Middleware = MiddlewareHandler<any, any, any>;
export interface ClassController {
getController: () => Hono<any, any, any>;
getMiddleware?: MiddlewareHandler<any, any, any>;
}

View File

@@ -1,62 +1,61 @@
import { get, has, omit, set } from "lodash-es"; import { get, has, omit, set } from "lodash-es";
import { import { type s, parse, stripMark, getFullPathKeys, mergeObjectWith, deepFreeze } from "bknd/utils";
Default,
type Static,
type TObject,
getFullPathKeys,
mergeObjectWith,
parse,
stripMark,
} from "../utils";
export type SchemaObjectOptions<Schema extends TObject> = { export type SchemaObjectOptions<Schema extends s.Schema> = {
onUpdate?: (config: Static<Schema>) => void | Promise<void>; onUpdate?: (config: s.Static<Schema>) => void | Promise<void>;
onBeforeUpdate?: ( onBeforeUpdate?: (
from: Static<Schema>, from: s.Static<Schema>,
to: Static<Schema>, to: s.Static<Schema>,
) => Static<Schema> | Promise<Static<Schema>>; ) => s.Static<Schema> | Promise<s.Static<Schema>>;
restrictPaths?: string[]; restrictPaths?: string[];
overwritePaths?: (RegExp | string)[]; overwritePaths?: (RegExp | string)[];
forceParse?: boolean; forceParse?: boolean;
}; };
export class SchemaObject<Schema extends TObject> { type TSchema = s.ObjectSchema<any>;
private readonly _default: Partial<Static<Schema>>;
private _value: Static<Schema>; export class SchemaObject<Schema extends TSchema = TSchema> {
private _config: Static<Schema>; private readonly _default: Partial<s.Static<Schema>>;
private _value: s.Static<Schema>;
private _config: s.Static<Schema>;
private _restriction_bypass: boolean = false; private _restriction_bypass: boolean = false;
constructor( constructor(
private _schema: Schema, private _schema: Schema,
initial?: Partial<Static<Schema>>, initial?: Partial<s.Static<Schema>>,
private options?: SchemaObjectOptions<Schema>, private options?: SchemaObjectOptions<Schema>,
) { ) {
this._default = Default(_schema, {} as any) as any; this._default = deepFreeze(_schema.template({}, { withOptional: true }) as any);
this._value = initial this._value = deepFreeze(
? parse(_schema, structuredClone(initial as any), { parse(_schema, structuredClone(initial ?? {}), {
forceParse: this.isForceParse(), withDefaults: true,
skipMark: this.isForceParse(), //withExtendedDefaults: true,
}) forceParse: this.isForceParse(),
: this._default; skipMark: this.isForceParse(),
this._config = Object.freeze(this._value); }),
);
this._config = deepFreeze(this._value);
} }
protected isForceParse(): boolean { protected isForceParse(): boolean {
return this.options?.forceParse ?? true; return this.options?.forceParse ?? true;
} }
default(): Static<Schema> { default() {
return this._default; return this._default;
} }
private async onBeforeUpdate(from: Static<Schema>, to: Static<Schema>): Promise<Static<Schema>> { private async onBeforeUpdate(
from: s.Static<Schema>,
to: s.Static<Schema>,
): Promise<s.Static<Schema>> {
if (this.options?.onBeforeUpdate) { if (this.options?.onBeforeUpdate) {
return this.options.onBeforeUpdate(from, to); return this.options.onBeforeUpdate(from, to);
} }
return to; return to;
} }
get(options?: { stripMark?: boolean }): Static<Schema> { get(options?: { stripMark?: boolean }): s.Static<Schema> {
if (options?.stripMark) { if (options?.stripMark) {
return stripMark(this._config); return stripMark(this._config);
} }
@@ -68,8 +67,9 @@ export class SchemaObject<Schema extends TObject> {
return structuredClone(this._config); return structuredClone(this._config);
} }
async set(config: Static<Schema>, noEmit?: boolean): Promise<Static<Schema>> { async set(config: s.Static<Schema>, noEmit?: boolean): Promise<s.Static<Schema>> {
const valid = parse(this._schema, structuredClone(config) as any, { const valid = parse(this._schema, structuredClone(config) as any, {
coerce: false,
forceParse: true, forceParse: true,
skipMark: this.isForceParse(), skipMark: this.isForceParse(),
}); });
@@ -77,8 +77,8 @@ export class SchemaObject<Schema extends TObject> {
// regardless of "noEmit" this should always be triggered // regardless of "noEmit" this should always be triggered
const updatedConfig = await this.onBeforeUpdate(this._config, valid); const updatedConfig = await this.onBeforeUpdate(this._config, valid);
this._value = updatedConfig; this._value = deepFreeze(updatedConfig);
this._config = Object.freeze(updatedConfig); this._config = deepFreeze(updatedConfig);
if (noEmit !== true) { if (noEmit !== true) {
await this.options?.onUpdate?.(this._config); await this.options?.onUpdate?.(this._config);
@@ -118,9 +118,9 @@ export class SchemaObject<Schema extends TObject> {
return; return;
} }
async patch(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> { async patch(path: string, value: any): Promise<[Partial<s.Static<Schema>>, s.Static<Schema>]> {
const current = this.clone(); const current = this.clone();
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value; const partial = path.length > 0 ? (set({}, path, value) as Partial<s.Static<Schema>>) : value;
this.throwIfRestricted(partial); this.throwIfRestricted(partial);
@@ -168,9 +168,12 @@ export class SchemaObject<Schema extends TObject> {
return [partial, newConfig]; return [partial, newConfig];
} }
async overwrite(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> { async overwrite(
path: string,
value: any,
): Promise<[Partial<s.Static<Schema>>, s.Static<Schema>]> {
const current = this.clone(); const current = this.clone();
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value; const partial = path.length > 0 ? (set({}, path, value) as Partial<s.Static<Schema>>) : value;
this.throwIfRestricted(partial); this.throwIfRestricted(partial);
@@ -194,7 +197,7 @@ export class SchemaObject<Schema extends TObject> {
return has(this._config, path); return has(this._config, path);
} }
async remove(path: string): Promise<[Partial<Static<Schema>>, Static<Schema>]> { async remove(path: string): Promise<[Partial<s.Static<Schema>>, s.Static<Schema>]> {
this.throwIfRestricted(path); this.throwIfRestricted(path);
if (!this.has(path)) { if (!this.has(path)) {
@@ -202,9 +205,9 @@ export class SchemaObject<Schema extends TObject> {
} }
const current = this.clone(); const current = this.clone();
const removed = get(current, path) as Partial<Static<Schema>>; const removed = get(current, path) as Partial<s.Static<Schema>>;
const config = omit(current, path); const config = omit(current, path);
const newConfig = await this.set(config); const newConfig = await this.set(config as any);
return [removed, newConfig]; return [removed, newConfig];
} }
} }

View File

@@ -1,4 +1,4 @@
import type { PrimaryFieldType } from "core"; import type { PrimaryFieldType } from "core/config";
export type Primitive = PrimaryFieldType | string | number | boolean; export type Primitive = PrimaryFieldType | string | number | boolean;
export function isPrimitive(value: any): value is Primitive { export function isPrimitive(value: any): value is Primitive {

View File

@@ -1,52 +0,0 @@
import { mergeObject } from "core/utils";
//export { jsc, type Options, type Hook } from "./validator";
import * as s from "jsonv-ts";
export { validator as jsc, type Options } from "jsonv-ts/hono";
export { describeRoute, schemaToSpec, openAPISpecs } from "jsonv-ts/hono";
export { s };
export class InvalidSchemaError extends Error {
constructor(
public schema: s.TAnySchema,
public value: unknown,
public errors: s.ErrorDetail[] = [],
) {
super(
`Invalid schema given for ${JSON.stringify(value, null, 2)}\n\n` +
`Error: ${JSON.stringify(errors[0], null, 2)}`,
);
}
}
export type ParseOptions = {
withDefaults?: boolean;
coerse?: boolean;
clone?: boolean;
};
export const cloneSchema = <S extends s.TSchema>(schema: S): S => {
const json = schema.toJSON();
return s.fromSchema(json) as S;
};
export function parse<S extends s.TAnySchema>(
_schema: S,
v: unknown,
opts: ParseOptions = {},
): s.StaticCoerced<S> {
const schema = (opts.clone ? cloneSchema(_schema as any) : _schema) as s.TSchema;
const value = opts.coerse !== false ? schema.coerce(v) : v;
const result = schema.validate(value, {
shortCircuit: true,
ignoreUnsupported: true,
});
if (!result.valid) throw new InvalidSchemaError(schema, v, result.errors);
if (opts.withDefaults) {
return mergeObject(schema.template({ withOptional: true }), value) as any;
}
return value as any;
}

View File

@@ -1,63 +0,0 @@
import type { Context, Env, Input, MiddlewareHandler, ValidationTargets } from "hono";
import { validator as honoValidator } from "hono/validator";
import type { Static, StaticCoerced, TAnySchema } from "jsonv-ts";
export type Options = {
coerce?: boolean;
includeSchema?: boolean;
};
type ValidationResult = {
valid: boolean;
errors: {
keywordLocation: string;
instanceLocation: string;
error: string;
data?: unknown;
}[];
};
export type Hook<T, E extends Env, P extends string> = (
result: { result: ValidationResult; data: T },
c: Context<E, P>,
) => Response | Promise<Response> | void;
export const validator = <
// @todo: somehow hono prevents the usage of TSchema
Schema extends TAnySchema,
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
Opts extends Options = Options,
Out = Opts extends { coerce: false } ? Static<Schema> : StaticCoerced<Schema>,
I extends Input = {
in: { [K in Target]: Static<Schema> };
out: { [K in Target]: Out };
},
>(
target: Target,
schema: Schema,
options?: Opts,
hook?: Hook<Out, E, P>,
): MiddlewareHandler<E, P, I> => {
// @ts-expect-error not typed well
return honoValidator(target, async (_value, c) => {
const value = options?.coerce !== false ? schema.coerce(_value) : _value;
// @ts-ignore
const result = schema.validate(value);
if (!result.valid) {
return c.json({ ...result, schema }, 400);
}
if (hook) {
const hookResult = hook({ result, data: value as Out }, c);
if (hookResult) {
return hookResult;
}
}
return value as Out;
});
};
export const jsc = validator;

View File

@@ -1 +0,0 @@
export { tbValidator } from "./tbValidator";

View File

@@ -1,37 +0,0 @@
import type { StaticDecode, TSchema } from "@sinclair/typebox";
import { Value, type ValueError } from "@sinclair/typebox/value";
import type { Context, Env, MiddlewareHandler, ValidationTargets } from "hono";
import { validator } from "hono/validator";
type Hook<T, E extends Env, P extends string> = (
result: { success: true; data: T } | { success: false; errors: ValueError[] },
c: Context<E, P>,
) => Response | Promise<Response> | void;
export function tbValidator<
T extends TSchema,
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
V extends { in: { [K in Target]: StaticDecode<T> }; out: { [K in Target]: StaticDecode<T> } },
>(target: Target, schema: T, hook?: Hook<StaticDecode<T>, E, P>): MiddlewareHandler<E, P, V> {
// Compile the provided schema once rather than per validation. This could be optimized further using a shared schema
// compilation pool similar to the Fastify implementation.
// @ts-expect-error not typed well
return validator(target, (data, c) => {
if (Value.Check(schema, data)) {
// always decode
const decoded = Value.Decode(schema, data);
if (hook) {
const hookResult = hook({ success: true, data: decoded }, c);
if (hookResult instanceof Response || hookResult instanceof Promise) {
return hookResult;
}
}
return decoded;
}
return c.json({ success: false, errors: [...Value.Errors(schema, data)] }, 400);
});
}

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { SimpleRenderer } from "core"; import { SimpleRenderer } from "./SimpleRenderer";
describe(SimpleRenderer, () => { describe(SimpleRenderer, () => {
const renderer = new SimpleRenderer( const renderer = new SimpleRenderer(

View File

@@ -1,6 +1,6 @@
import { datetimeStringLocal } from "core/utils"; import { datetimeStringLocal } from "./dates";
import colors from "picocolors"; import colors from "picocolors";
import { env } from "core"; import { env } from "core/env";
function hasColors() { function hasColors() {
try { try {

View File

@@ -6,12 +6,25 @@ export * from "./perf";
export * from "./file"; export * from "./file";
export * from "./reqres"; export * from "./reqres";
export * from "./xml"; export * from "./xml";
export type { Prettify, PrettifyRec } from "./types"; export type { Prettify, PrettifyRec, RecursivePartial } from "./types";
export * from "./typebox";
export * from "./dates"; export * from "./dates";
export * from "./crypto"; export * from "./crypto";
export * from "./uuid"; export * from "./uuid";
export { FromSchema } from "./typebox/from-schema";
export * from "./test"; export * from "./test";
export * from "./runtime"; export * from "./runtime";
export * from "./numbers"; export * from "./numbers";
export {
s,
stripMark,
mark,
stringIdentifier,
SecretSchema,
secret,
parse,
jsc,
describeRoute,
schemaToSpec,
openAPISpecs,
type ParseOptions,
InvalidSchemaError,
} from "./schema";

Some files were not shown because too many files have changed in this diff Show More