Merge pull request #193 from bknd-io/feat/refactor-sqlites

feat/refactor-sqlites
This commit is contained in:
dswbx
2025-07-02 14:05:58 +02:00
committed by GitHub
72 changed files with 845 additions and 353 deletions

View File

@@ -60,7 +60,14 @@ function banner(title: string) {
}
// collection of always-external packages
const external = ["bun:test", "node:test", "node:assert/strict", "@libsql/client"] as const;
const external = [
"bun:test",
"node:test",
"node:assert/strict",
"@libsql/client",
"bknd",
/^bknd\/.*/,
] as const;
/**
* Building backend and general API

View File

@@ -65,6 +65,7 @@
"json-schema-form-react": "^0.0.2",
"json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1",
"jsonv-ts": "^0.1.0",
"kysely": "^0.27.6",
"lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1",
@@ -82,7 +83,6 @@
"@hono/vite-dev-server": "^0.19.1",
"@hookform/resolvers": "^4.1.3",
"@libsql/client": "^0.15.9",
"@libsql/kysely-libsql": "^0.4.1",
"@mantine/modals": "^7.17.1",
"@mantine/notifications": "^7.17.1",
"@playwright/test": "^1.51.1",
@@ -102,7 +102,6 @@
"dotenv": "^16.4.7",
"jotai": "^2.12.2",
"jsdom": "^26.0.0",
"jsonv-ts": "^0.1.0",
"kysely-d1": "^0.3.0",
"kysely-generic-sqlite": "^1.2.1",
"libsql-stateless-easy": "^1.8.0",
@@ -187,6 +186,11 @@
"import": "./dist/media/index.js",
"require": "./dist/media/index.js"
},
"./plugins": {
"types": "./dist/types/plugins/index.d.ts",
"import": "./dist/plugins/index.js",
"require": "./dist/plugins/index.js"
},
"./adapter/sqlite": {
"types": "./dist/types/adapter/sqlite/edge.d.ts",
"import": {
@@ -201,11 +205,6 @@
},
"require": "./dist/adapter/sqlite/node.js"
},
"./plugins": {
"types": "./dist/types/plugins/index.d.ts",
"import": "./dist/plugins/index.js",
"require": "./dist/plugins/index.js"
},
"./adapter/cloudflare": {
"types": "./dist/types/adapter/cloudflare/index.d.ts",
"import": "./dist/adapter/cloudflare/index.js",
@@ -262,14 +261,14 @@
"cli": ["./dist/types/cli/index.d.ts"],
"media": ["./dist/types/media/index.d.ts"],
"plugins": ["./dist/types/plugins/index.d.ts"],
"sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"],
"adapter": ["./dist/types/adapter/index.d.ts"],
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
"adapter/vite": ["./dist/types/adapter/vite/index.d.ts"],
"adapter/nextjs": ["./dist/types/adapter/nextjs/index.d.ts"],
"adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
"adapter/node": ["./dist/types/adapter/node/index.d.ts"]
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
}
},
"publishConfig": {

View File

@@ -1,5 +1,5 @@
import type { CreateUserPayload } from "auth/AppAuth";
import { $console } from "core";
import { $console } from "core/utils";
import { Event } from "core/events";
import type { em as prototypeEm } from "data/prototype";
import { Connection } from "data/connection/Connection";
@@ -34,7 +34,10 @@ export type AppPluginConfig = {
export type AppPlugin = (app: App) => AppPluginConfig;
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
export class AppConfigUpdatedEvent extends AppEvent {
export class AppConfigUpdatedEvent extends AppEvent<{
module: string;
config: ModuleConfigs[keyof ModuleConfigs];
}> {
static override slug = "app-config-updated";
}
export class AppBuiltEvent extends AppEvent {
@@ -265,7 +268,7 @@ export class App<C extends Connection = Connection, Options extends AppOptions =
$console.log("App config updated", module);
// @todo: potentially double syncing
await this.build({ sync: true });
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this, module, config }));
}
protected async onFirstBoot() {

View File

@@ -6,7 +6,10 @@ import { Database } from "bun:sqlite";
describe("BunSqliteConnection", () => {
connectionTestSuite(bunTestRunner, {
makeConnection: () => bunSqlite({ database: new Database(":memory:") }),
makeConnection: () => ({
connection: bunSqlite({ database: new Database(":memory:") }),
dispose: async () => {},
}),
rawDialectDetails: [],
});
});

View File

@@ -1,8 +1,11 @@
import { expect, test, mock, describe } from "bun:test";
import { expect, test, mock, describe, beforeEach, afterEach, afterAll } from "bun:test";
export const bunTestRunner = {
describe,
expect,
test,
mock,
beforeEach,
afterEach,
afterAll,
};

View File

@@ -7,7 +7,7 @@ import { getFresh } from "./modes/fresh";
import { getCached } from "./modes/cached";
import { getDurable } from "./modes/durable";
import type { App } from "bknd";
import { $console } from "core";
import { $console } from "core/utils";
declare global {
namespace Cloudflare {

View File

@@ -2,12 +2,13 @@
import { registerMedia } from "./storage/StorageR2Adapter";
import { getBinding } from "./bindings";
import { D1Connection } from "./connection/D1Connection";
import { d1Sqlite } from "./connection/D1Connection";
import { Connection } from "bknd/data";
import type { CloudflareBkndConfig, CloudflareEnv } from ".";
import { App } from "bknd";
import { makeConfig as makeAdapterConfig } from "bknd/adapter";
import type { Context, ExecutionContext } from "hono";
import { $console } from "core";
import { $console } from "core/utils";
import { setCookie } from "hono/cookie";
import { sqlite } from "bknd/adapter/sqlite";
@@ -101,7 +102,7 @@ export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
// if connection instance is given, don't do anything
// other than checking if D1 session is defined
if (D1Connection.isConnection(appConfig.connection)) {
if (Connection.isConnection(appConfig.connection)) {
if (config.d1?.session) {
// we cannot guarantee that db was opened with session
throw new Error(
@@ -139,8 +140,11 @@ export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
if (db) {
if (config.d1?.session) {
session = db.withSession(sessionId ?? config.d1?.first);
if (!session) {
throw new Error("Couldn't create session");
}
appConfig.connection = new D1Connection({ binding: session });
appConfig.connection = d1Sqlite({ binding: session });
appConfig.options = {
...appConfig.options,
manager: {
@@ -154,12 +158,12 @@ export function makeConfig<Env extends CloudflareEnv = CloudflareEnv>(
},
};
} else {
appConfig.connection = new D1Connection({ binding: db });
appConfig.connection = d1Sqlite({ binding: db });
}
}
}
if (!D1Connection.isConnection(appConfig.connection)) {
if (!Connection.isConnection(appConfig.connection)) {
throw new Error("Couldn't find database connection");
}

View File

@@ -1,42 +1,78 @@
/// <reference types="@cloudflare/workers-types" />
import { SqliteConnection } from "bknd/data";
import type { ConnQuery, ConnQueryResults } from "data/connection/Connection";
import { D1Dialect } from "kysely-d1";
import {
genericSqlite,
type GenericSqliteConnection,
} from "data/connection/sqlite/GenericSqliteConnection";
import type { QueryResult } from "kysely";
export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
export type D1ConnectionConfig<DB extends D1Database | D1DatabaseSession = D1Database> = {
binding: DB;
};
export class D1Connection<
DB extends D1Database | D1DatabaseSession = D1Database,
> extends SqliteConnection<DB> {
override name = "sqlite-d1";
export function d1Sqlite<DB extends D1Database | D1DatabaseSession = D1Database>(
config: D1ConnectionConfig<DB>,
) {
const db = config.binding;
protected override readonly supported = {
batching: true,
softscans: false,
};
return genericSqlite(
"d1-sqlite",
db,
(utils) => {
const getStmt = (sql: string, parameters?: any[] | readonly any[]) =>
db.prepare(sql).bind(...(parameters || []));
constructor(private config: D1ConnectionConfig<DB>) {
super({
const mapResult = (res: D1Result<any>): QueryResult<any> => {
if (res.error) {
throw new Error(res.error);
}
const numAffectedRows =
res.meta.changes > 0 ? utils.parseBigInt(res.meta.changes) : undefined;
const insertId = res.meta.last_row_id
? utils.parseBigInt(res.meta.last_row_id)
: undefined;
return {
insertId,
numAffectedRows,
rows: res.results,
// @ts-ignore
meta: res.meta,
};
};
return {
db,
batch: async (stmts) => {
const res = await db.batch(
stmts.map(({ sql, parameters }) => {
return getStmt(sql, parameters);
}),
);
return res.map(mapResult);
},
query: utils.buildQueryFn({
all: async (sql, parameters) => {
const prep = getStmt(sql, parameters);
return mapResult(await prep.all()).rows;
},
run: async (sql, parameters) => {
const prep = getStmt(sql, parameters);
return mapResult(await prep.run());
},
}),
close: () => {},
};
},
{
supports: {
batching: true,
softscans: false,
},
excludeTables: ["_cf_KV", "_cf_METADATA"],
dialect: D1Dialect,
dialectArgs: [{ database: config.binding as D1Database }],
});
}
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
const compiled = this.getCompiled(...qbs);
const db = this.config.binding;
const res = await db.batch(
compiled.map(({ sql, parameters }) => {
return db.prepare(sql).bind(...parameters);
}),
);
return this.withTransformedRows(res, "results") as any;
}
},
);
}

View File

@@ -0,0 +1,33 @@
import { describe, test, expect } from "vitest";
import { viTestRunner } from "adapter/node/vitest";
import { connectionTestSuite } from "data/connection/connection-test-suite";
import { Miniflare } from "miniflare";
import { d1Sqlite } from "./D1Connection";
describe("d1Sqlite", async () => {
connectionTestSuite(viTestRunner, {
makeConnection: async () => {
const mf = new Miniflare({
modules: true,
script: "export default { async fetch() { return new Response(null); } }",
d1Databases: ["DB"],
});
const binding = (await mf.getD1Database("DB")) as D1Database;
return {
connection: d1Sqlite({ binding }),
dispose: () => mf.dispose(),
};
},
rawDialectDetails: [
"meta.served_by",
"meta.duration",
"meta.changes",
"meta.changed_db",
"meta.size_after",
"meta.rows_read",
"meta.rows_written",
],
});
});

View File

@@ -0,0 +1,83 @@
/// <reference types="@cloudflare/workers-types" />
import {
genericSqlite,
type GenericSqliteConnection,
} from "data/connection/sqlite/GenericSqliteConnection";
import type { QueryResult } from "kysely";
export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
export type DurableObjecSql = DurableObjectState["storage"]["sql"];
export type D1ConnectionConfig<DB extends DurableObjecSql> =
| DurableObjectState
| {
sql: DB;
};
export function doSqlite<DB extends DurableObjecSql>(config: D1ConnectionConfig<DB>) {
const db = "sql" in config ? config.sql : config.storage.sql;
return genericSqlite(
"do-sqlite",
db,
(utils) => {
// must be async to work with the miniflare mock
const getStmt = async (sql: string, parameters?: any[] | readonly any[]) =>
await db.exec(sql, ...(parameters || []));
const mapResult = (
cursor: SqlStorageCursor<Record<string, SqlStorageValue>>,
): QueryResult<any> => {
const numAffectedRows =
cursor.rowsWritten > 0 ? utils.parseBigInt(cursor.rowsWritten) : undefined;
const insertId = undefined;
const obj = {
insertId,
numAffectedRows,
rows: cursor.toArray() || [],
// @ts-ignore
meta: {
rowsWritten: cursor.rowsWritten,
rowsRead: cursor.rowsRead,
databaseSize: db.databaseSize,
},
};
//console.info("mapResult", obj);
return obj;
};
return {
db,
batch: async (stmts) => {
// @todo: maybe wrap in a transaction?
// because d1 implicitly does a transaction on batch
return Promise.all(
stmts.map(async (stmt) => {
return mapResult(await getStmt(stmt.sql, stmt.parameters));
}),
);
},
query: utils.buildQueryFn({
all: async (sql, parameters) => {
const prep = getStmt(sql, parameters);
return mapResult(await prep).rows;
},
run: async (sql, parameters) => {
const prep = getStmt(sql, parameters);
return mapResult(await prep);
},
}),
close: () => {},
};
},
{
supports: {
batching: true,
softscans: false,
},
excludeTables: ["_cf_KV", "_cf_METADATA"],
},
);
}

View File

@@ -0,0 +1,92 @@
/// <reference types="@cloudflare/workers-types" />
import { describe, test, expect } from "vitest";
import { viTestRunner } from "adapter/node/vitest";
import { connectionTestSuite } from "data/connection/connection-test-suite";
import { Miniflare } from "miniflare";
import { doSqlite } from "./DoConnection";
const script = `
import { DurableObject } from "cloudflare:workers";
export class TestObject extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
this.storage = ctx.storage;
}
async exec(sql, ...parameters) {
//return { sql, parameters }
const cursor = this.storage.sql.exec(sql, ...parameters);
return {
rows: cursor.toArray() || [],
rowsWritten: cursor.rowsWritten,
rowsRead: cursor.rowsRead,
databaseSize: this.storage.sql.databaseSize,
}
}
async databaseSize() {
return this.storage.sql.databaseSize;
}
}
export default {
async fetch(request, env) {
const stub = env.TEST_OBJECT.get(env.TEST_OBJECT.idFromName("test"));
return stub.fetch(request);
}
}
`;
describe("doSqlite", async () => {
connectionTestSuite(viTestRunner, {
makeConnection: async () => {
const mf = new Miniflare({
modules: true,
durableObjects: { TEST_OBJECT: { className: "TestObject", useSQLite: true } },
script,
});
const ns = await mf.getDurableObjectNamespace("TEST_OBJECT");
const id = ns.idFromName("test");
const stub = ns.get(id) as unknown as DurableObjectStub<
Rpc.DurableObjectBranded & {
exec: (sql: string, ...parameters: any[]) => Promise<any>;
}
>;
const stubs: any[] = [];
const mock = {
databaseSize: 0,
exec: async function (sql: string, ...parameters: any[]) {
// @ts-ignore
const result = (await stub.exec(sql, ...parameters)) as any;
this.databaseSize = result.databaseSize;
stubs.push(result);
return {
toArray: () => result.rows,
rowsWritten: result.rowsWritten,
rowsRead: result.rowsRead,
};
},
};
return {
connection: doSqlite({ sql: mock as any }),
dispose: async () => {
await Promise.all(
stubs.map((stub) => {
try {
return stub[Symbol.dispose]();
} catch (e) {}
}),
);
await mf.dispose();
},
};
},
rawDialectDetails: ["meta.rowsWritten", "meta.rowsRead", "meta.databaseSize"],
});
});

View File

@@ -1,10 +1,10 @@
import { D1Connection, type D1ConnectionConfig } from "./connection/D1Connection";
import { d1Sqlite, type D1ConnectionConfig } from "./connection/D1Connection";
export * from "./cloudflare-workers.adapter";
export { makeApp, getFresh } from "./modes/fresh";
export { getCached } from "./modes/cached";
export { DurableBkndApp, getDurable } from "./modes/durable";
export { D1Connection, type D1ConnectionConfig };
export { d1Sqlite, type D1ConnectionConfig };
export {
getBinding,
getBindings,
@@ -14,6 +14,9 @@ export {
} from "./bindings";
export { constants } from "./config";
export function d1(config: D1ConnectionConfig) {
return new D1Connection(config);
// for compatibility with old code
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
config: D1ConnectionConfig<DB>,
) {
return d1Sqlite<DB>(config);
}

View File

@@ -3,7 +3,7 @@ import type { App, CreateAppConfig } from "bknd";
import { createRuntimeApp, makeConfig } from "bknd/adapter";
import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index";
import { constants, registerAsyncsExecutionContext } from "../config";
import { $console } from "core";
import { $console } from "core/utils";
export async function getDurable<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env>,

View File

@@ -1,5 +1,6 @@
import { App, type CreateAppConfig } from "bknd";
import { config as $config, $console } from "bknd/core";
import { config as $config } from "bknd/core";
import { $console } from "bknd/utils";
import type { MiddlewareHandler } from "hono";
import type { AdminControllerOptions } from "modules/server/AdminController";
import { Connection } from "bknd/data";

View File

@@ -17,32 +17,41 @@ export function nodeSqlite(config?: NodeSqliteConnectionConfig | { url: string }
db = new DatabaseSync(":memory:");
}
return genericSqlite("node-sqlite", db, (utils) => {
const getStmt = (sql: string) => {
const stmt = db.prepare(sql);
//stmt.setReadBigInts(true);
return stmt;
};
return genericSqlite(
"node-sqlite",
db,
(utils) => {
const getStmt = (sql: string) => {
const stmt = db.prepare(sql);
//stmt.setReadBigInts(true);
return stmt;
};
return {
db,
query: utils.buildQueryFn({
all: (sql, parameters = []) => getStmt(sql).all(...parameters),
run: (sql, parameters = []) => {
const { changes, lastInsertRowid } = getStmt(sql).run(...parameters);
return {
insertId: utils.parseBigInt(lastInsertRowid),
numAffectedRows: utils.parseBigInt(changes),
};
return {
db,
query: utils.buildQueryFn({
all: (sql, parameters = []) => getStmt(sql).all(...parameters),
run: (sql, parameters = []) => {
const { changes, lastInsertRowid } = getStmt(sql).run(...parameters);
return {
insertId: utils.parseBigInt(lastInsertRowid),
numAffectedRows: utils.parseBigInt(changes),
};
},
}),
close: () => db.close(),
iterator: (isSelect, sql, parameters = []) => {
if (!isSelect) {
throw new Error("Only support select in stream()");
}
return getStmt(sql).iterate(...parameters) as any;
},
}),
close: () => db.close(),
iterator: (isSelect, sql, parameters = []) => {
if (!isSelect) {
throw new Error("Only support select in stream()");
}
return getStmt(sql).iterate(...parameters) as any;
};
},
{
supports: {
batching: false,
},
};
});
},
);
}

View File

@@ -1,11 +1,15 @@
import { nodeSqlite } from "./NodeSqliteConnection";
import { DatabaseSync } from "node:sqlite";
import { connectionTestSuite } from "data/connection/connection-test-suite";
import { describe, test, expect } from "vitest";
import { describe } from "vitest";
import { viTestRunner } from "../vitest";
describe("NodeSqliteConnection", () => {
connectionTestSuite({ describe, test, expect } as any, {
makeConnection: () => nodeSqlite({ database: new DatabaseSync(":memory:") }),
connectionTestSuite(viTestRunner, {
makeConnection: () => ({
connection: nodeSqlite({ database: new DatabaseSync(":memory:") }),
dispose: async () => {},
}),
rawDialectDetails: [],
});
});

View File

@@ -4,7 +4,7 @@ import { serveStatic } from "@hono/node-server/serve-static";
import { registerLocalMediaAdapter } from "adapter/node/storage";
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import { config as $config } from "bknd/core";
import { $console } from "core";
import { $console } from "core/utils";
import type { App } from "App";
type NodeEnv = NodeJS.ProcessEnv;

View File

@@ -1,5 +1,5 @@
import nodeAssert from "node:assert/strict";
import { test, describe } from "node:test";
import { test, describe, beforeEach, afterEach } from "node:test";
import type { Matcher, Test, TestFn, TestRunner } from "core/test";
// Track mock function calls
@@ -97,4 +97,7 @@ export const nodeTestRunner: TestRunner = {
reject: (r) => nodeTestMatcher(r, failMsg),
}),
}),
beforeEach: beforeEach,
afterEach: afterEach,
afterAll: () => {},
};

View File

@@ -1,5 +1,5 @@
import type { TestFn, TestRunner, Test } from "core/test";
import { describe, test, expect, vi } from "vitest";
import { describe, test, expect, vi, beforeEach, afterEach, afterAll } from "vitest";
function vitestTest(label: string, fn: TestFn, options?: any) {
return test(label, fn as any);
@@ -47,4 +47,7 @@ export const viTestRunner: TestRunner = {
test: vitestTest,
expect: vitestExpect as any,
mock: (fn) => vi.fn(fn),
beforeEach: beforeEach,
afterEach: afterEach,
afterAll: afterAll,
};

View File

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

View File

@@ -1,3 +0,0 @@
import type { Connection } from "bknd/data";
export type SqliteConnection = (config: { url: string }) => Connection;

View File

@@ -1,7 +1,7 @@
import { Authenticator, AuthPermissions, Role, type Strategy } from "auth";
import type { PasswordStrategy } from "auth/authenticate/strategies";
import { $console, type DB } from "core";
import { secureRandomString, transformObject } from "core/utils";
import type { DB } from "core";
import { $console, secureRandomString, transformObject } from "core/utils";
import type { Entity, EntityManager } from "data";
import { em, entity, enumm, type FieldSchema, text } from "data/prototype";
import { Module } from "modules/Module";

View File

@@ -1,6 +1,6 @@
import { AppAuth } from "auth/AppAuth";
import type { CreateUser, SafeUser, User, UserPool } from "auth/authenticate/Authenticator";
import { $console } from "core";
import { $console } from "core/utils";
import { pick } from "lodash-es";
import {
InvalidConditionsException,

View File

@@ -1,6 +1,7 @@
import { $console, type DB, Exception } from "core";
import { type DB, Exception } from "core";
import { addFlashMessage } from "core/server/flash";
import {
$console,
type Static,
StringEnum,
type TObject,

View File

@@ -1,6 +1,6 @@
import { type Authenticator, InvalidCredentialsException, type User } from "auth";
import { $console, tbValidator as tb } from "core";
import { hash, parse, type Static, StrictObject, StringEnum } from "core/utils";
import { tbValidator as tb } from "core";
import { $console, hash, parse, type Static, StrictObject, StringEnum } from "core/utils";
import { Hono } from "hono";
import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs";
import * as tbbox from "@sinclair/typebox";

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import path from "node:path";
import { $console } from "core";
import { $console } from "core/utils";
import type { MiddlewareHandler } from "hono";
import open from "open";
import { fileExists, getRelativeDistPath } from "../../utils/sys";

View File

@@ -3,7 +3,7 @@ import type { App, CreateAppConfig } from "App";
import { StorageLocalAdapter } from "adapter/node/storage";
import type { CliBkndConfig, CliCommand } from "cli/types";
import { Option } from "commander";
import { colorizeConsole, config } from "core";
import { config } from "core";
import dotenv from "dotenv";
import { registries } from "modules/registries";
import c from "picocolors";
@@ -17,7 +17,7 @@ import {
startServer,
} from "./platform";
import { createRuntimeApp, makeConfig } from "adapter";
import { isBun } from "core/utils";
import { colorizeConsole, isBun } from "core/utils";
const env_files = [".env", ".dev.vars"];
dotenv.config({

View File

@@ -9,7 +9,7 @@ import type { PasswordStrategy } from "auth/authenticate/strategies";
import { makeAppFromEnv } from "cli/commands/run";
import type { CliCommand } from "cli/types";
import { Argument } from "commander";
import { $console } from "core";
import { $console } from "core/utils";
import c from "picocolors";
import { isBun } from "core/utils";

View File

@@ -1,4 +1,4 @@
import { $console } from "core";
import { $console } from "core/utils";
import { execSync, exec as nodeExec } from "node:child_process";
import { readFile, writeFile as nodeWriteFile } from "node:fs/promises";
import path from "node:path";

View File

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

View File

@@ -1,6 +1,6 @@
import { type Event, type EventClass, InvalidEventReturn } from "./Event";
import { EventListener, type ListenerHandler, type ListenerMode } from "./EventListener";
import { $console } from "core";
import { $console } from "core/utils";
export type RegisterListenerConfig =
| ListenerMode

View File

@@ -38,7 +38,6 @@ export {
} from "./object/schema";
export * from "./drivers";
export * from "./console";
export * from "./events";
// compatibility

View File

@@ -1,3 +1,5 @@
import type { MaybePromise } from "core/types";
export type Matcher<T = unknown> = {
toEqual: (expected: T, failMsg?: string) => void;
toBe: (expected: T, failMsg?: string) => void;
@@ -16,7 +18,7 @@ export interface Test {
skipIf: (condition: boolean) => (label: string, fn: TestFn) => void;
}
export type TestRunner = {
describe: (label: string, asyncFn: () => Promise<void>) => void;
describe: (label: string, asyncFn: () => MaybePromise<void>) => void;
test: Test;
mock: <T extends (...args: any[]) => any>(fn: T) => T | any;
expect: <T = unknown>(
@@ -26,6 +28,9 @@ export type TestRunner = {
resolves: Matcher<Awaited<T>>;
rejects: Matcher<Awaited<T>>;
};
beforeEach: (fn: () => MaybePromise<void>) => void;
afterEach: (fn: () => MaybePromise<void>) => void;
afterAll: (fn: () => MaybePromise<void>) => void;
};
export async function retry<T>(

View File

@@ -2,7 +2,7 @@ import { extension, guess, isMimeType } from "media/storage/mime-types-tiny";
import { randomString } from "core/utils/strings";
import type { Context } from "hono";
import { invariant } from "core/utils/runtime";
import { $console } from "../console";
import { $console } from "./console";
export function getContentName(request: Request): string | undefined;
export function getContentName(contentDisposition: string): string | undefined;

View File

@@ -1,3 +1,4 @@
export * from "./console";
export * from "./browser";
export * from "./objects";
export * from "./strings";

View File

@@ -1,4 +1,4 @@
import { $console } from "core";
import { $console } from "./console";
type ConsoleSeverity = "log" | "warn" | "error";
const _oldConsoles = {
@@ -36,14 +36,14 @@ export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"
severities.forEach((severity) => {
console[severity] = () => null;
});
$console.setLevel("critical");
$console?.setLevel("critical");
}
export function enableConsoleLog() {
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
console[severity as ConsoleSeverity] = fn;
});
$console.resetLevel();
$console?.resetLevel();
}
export function formatMemoryUsage() {

View File

@@ -1,4 +1,3 @@
import { $console, isDebug } from "core";
import {
DataPermissions,
type EntityData,

View File

@@ -1,5 +1,9 @@
import type { TestRunner } from "core/test";
import { Connection, type FieldSpec } from "./Connection";
import { getPath } from "core/utils";
import * as proto from "data/prototype";
import { createApp } from "App";
import type { MaybePromise } from "core/types";
// @todo: add various datatypes: string, number, boolean, object, array, null, undefined, date, etc.
// @todo: add toDriver/fromDriver tests on all types and fields
@@ -10,77 +14,92 @@ export function connectionTestSuite(
makeConnection,
rawDialectDetails,
}: {
makeConnection: () => Connection;
makeConnection: () => MaybePromise<{
connection: Connection;
dispose: () => MaybePromise<void>;
}>;
rawDialectDetails: string[];
},
) {
const { test, expect, describe } = testRunner;
const { test, expect, describe, beforeEach, afterEach, afterAll } = testRunner;
test("pings", async () => {
const connection = makeConnection();
const res = await connection.ping();
expect(res).toBe(true);
});
describe("base", () => {
let ctx: Awaited<ReturnType<typeof makeConnection>>;
beforeEach(async () => {
ctx = await makeConnection();
});
afterEach(async () => {
await ctx.dispose();
});
test("initializes", async () => {
const connection = makeConnection();
await connection.init();
// @ts-expect-error
expect(connection.initialized).toBe(true);
expect(connection.client).toBeDefined();
});
test("pings", async () => {
const res = await ctx.connection.ping();
expect(res).toBe(true);
});
test("isConnection", async () => {
const connection = makeConnection();
expect(Connection.isConnection(connection)).toBe(true);
});
test("getFieldSchema", async () => {
const c = makeConnection();
const specToNode = (spec: FieldSpec) => {
test("initializes", async () => {
await ctx.connection.init();
// @ts-expect-error
const schema = c.kysely.schema.createTable("test").addColumn(...c.getFieldSchema(spec));
return schema.toOperationNode();
};
expect(ctx.connection.initialized).toBe(true);
expect(ctx.connection.client).toBeDefined();
});
{
// primary
const node = specToNode({
type: "integer",
name: "id",
primary: true,
});
const col = node.columns[0]!;
expect(col.primaryKey).toBe(true);
expect(col.notNull).toBe(true);
}
test("isConnection", async () => {
expect(Connection.isConnection(ctx.connection)).toBe(true);
});
{
// normal
const node = specToNode({
type: "text",
name: "text",
});
const col = node.columns[0]!;
expect(!col.primaryKey).toBe(true);
expect(!col.notNull).toBe(true);
}
test("getFieldSchema", async () => {
const specToNode = (spec: FieldSpec) => {
const schema = ctx.connection.kysely.schema
.createTable("test")
// @ts-expect-error
.addColumn(...ctx.connection.getFieldSchema(spec));
return schema.toOperationNode();
};
{
// nullable (expect to be same as normal)
const node = specToNode({
type: "text",
name: "text",
nullable: true,
});
const col = node.columns[0]!;
expect(!col.primaryKey).toBe(true);
expect(!col.notNull).toBe(true);
}
{
// primary
const node = specToNode({
type: "integer",
name: "id",
primary: true,
});
const col = node.columns[0]!;
expect(col.primaryKey).toBe(true);
expect(col.notNull).toBe(true);
}
{
// normal
const node = specToNode({
type: "text",
name: "text",
});
const col = node.columns[0]!;
expect(!col.primaryKey).toBe(true);
expect(!col.notNull).toBe(true);
}
{
// nullable (expect to be same as normal)
const node = specToNode({
type: "text",
name: "text",
nullable: true,
});
const col = node.columns[0]!;
expect(!col.primaryKey).toBe(true);
expect(!col.notNull).toBe(true);
}
});
});
describe("schema", async () => {
const connection = makeConnection();
const { connection, dispose } = await makeConnection();
afterAll(async () => {
await dispose();
});
const fields = [
{
type: "integer",
@@ -118,14 +137,16 @@ export function connectionTestSuite(
const qb = connection.kysely.selectFrom("test").selectAll();
const res = await connection.executeQuery(qb);
expect(res.rows).toEqual([expected]);
expect(rawDialectDetails.every((detail) => detail in res)).toBe(true);
expect(rawDialectDetails.every((detail) => getPath(res, detail) !== undefined)).toBe(true);
{
const res = await connection.executeQueries(qb, qb);
expect(res.length).toBe(2);
res.map((r) => {
expect(r.rows).toEqual([expected]);
expect(rawDialectDetails.every((detail) => detail in r)).toBe(true);
expect(rawDialectDetails.every((detail) => getPath(r, detail) !== undefined)).toBe(
true,
);
});
}
});
@@ -187,4 +208,146 @@ export function connectionTestSuite(
},
]);
});
describe("integration", async () => {
let ctx: Awaited<ReturnType<typeof makeConnection>>;
beforeEach(async () => {
ctx = await makeConnection();
});
afterEach(async () => {
await ctx.dispose();
});
test("should create app and ping", async () => {
const app = createApp({
connection: ctx.connection,
});
await app.build();
expect(app.version()).toBeDefined();
expect(await app.em.ping()).toBe(true);
});
test("should create a basic schema", async () => {
const schema = proto.em(
{
posts: proto.entity("posts", {
title: proto.text().required(),
content: proto.text(),
}),
comments: proto.entity("comments", {
content: proto.text(),
}),
},
(fns, s) => {
fns.relation(s.comments).manyToOne(s.posts);
fns.index(s.posts).on(["title"], true);
},
);
const app = createApp({
connection: ctx.connection,
initialConfig: {
data: schema.toJSON(),
},
});
await app.build();
expect(app.em.entities.length).toBe(2);
expect(app.em.entities.map((e) => e.name)).toEqual(["posts", "comments"]);
const api = app.getApi();
expect(
(
await api.data.createMany("posts", [
{
title: "Hello",
content: "World",
},
{
title: "Hello 2",
content: "World 2",
},
])
).data,
).toEqual([
{
id: 1,
title: "Hello",
content: "World",
},
{
id: 2,
title: "Hello 2",
content: "World 2",
},
] as any);
// try to create an existing
expect(
(
await api.data.createOne("posts", {
title: "Hello",
})
).ok,
).toBe(false);
// add a comment to a post
await api.data.createOne("comments", {
content: "Hello",
posts_id: 1,
});
// and then query using a `with` property
const result = await api.data.readMany("posts", { with: ["comments"] });
expect(result.length).toBe(2);
expect(result[0]?.comments?.length).toBe(1);
expect(result[0]?.comments?.[0]?.content).toBe("Hello");
expect(result[1]?.comments?.length).toBe(0);
});
test("should support uuid", async () => {
const schema = proto.em(
{
posts: proto.entity(
"posts",
{
title: proto.text().required(),
content: proto.text(),
},
{
primary_format: "uuid",
},
),
comments: proto.entity("comments", {
content: proto.text(),
}),
},
(fns, s) => {
fns.relation(s.comments).manyToOne(s.posts);
fns.index(s.posts).on(["title"], true);
},
);
const app = createApp({
connection: ctx.connection,
initialConfig: {
data: schema.toJSON(),
},
});
await app.build();
const config = app.toJSON();
// @ts-expect-error
expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid");
const em = app.em;
const mutator = em.mutator(em.entity("posts"));
const data = await mutator.insertOne({ title: "Hello", content: "World" });
expect(data.data.id).toBeString();
expect(String(data.data.id).length).toBe(36);
});
});
}

View File

@@ -1,4 +1,4 @@
import type { KyselyPlugin } from "kysely";
import type { KyselyPlugin, QueryResult } from "kysely";
import {
type IGenericSqlite,
type OnCreateConnection,
@@ -8,11 +8,16 @@ import {
GenericSqliteDialect,
} from "kysely-generic-sqlite";
import { SqliteConnection } from "./SqliteConnection";
import type { Features } from "../Connection";
import type { ConnQuery, ConnQueryResults, Features } from "../Connection";
export type { IGenericSqlite };
export type TStatement = { sql: string; parameters?: any[] | readonly any[] };
export interface IGenericCustomSqlite<DB = unknown> extends IGenericSqlite<DB> {
batch?: (stmts: TStatement[]) => Promisable<QueryResult<any>[]>;
}
export type GenericSqliteConnectionConfig = {
name: string;
name?: string;
additionalPlugins?: KyselyPlugin[];
excludeTables?: string[];
onCreateConnection?: OnCreateConnection;
@@ -21,10 +26,11 @@ export type GenericSqliteConnectionConfig = {
export class GenericSqliteConnection<DB = unknown> extends SqliteConnection<DB> {
override name = "generic-sqlite";
#executor: IGenericCustomSqlite<DB> | undefined;
constructor(
db: DB,
executor: () => Promisable<IGenericSqlite>,
public db: DB,
private executor: () => Promisable<IGenericCustomSqlite<DB>>,
config?: GenericSqliteConnectionConfig,
) {
super({
@@ -39,18 +45,43 @@ export class GenericSqliteConnection<DB = unknown> extends SqliteConnection<DB>
}
if (config?.supports) {
for (const [key, value] of Object.entries(config.supports)) {
if (value) {
if (value !== undefined) {
this.supported[key] = value;
}
}
}
}
private async getExecutor() {
if (!this.#executor) {
this.#executor = await this.executor();
}
return this.#executor;
}
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
const executor = await this.getExecutor();
if (!executor.batch) {
//$console.debug("Batching is not supported by this database");
return super.executeQueries(...qbs);
}
const compiled = this.getCompiled(...qbs);
const stms: TStatement[] = compiled.map((q) => {
return {
sql: q.sql,
parameters: q.parameters as any[],
};
});
const results = await executor.batch(stms);
return this.withTransformedRows(results) as any;
}
}
export function genericSqlite<DB>(
name: string,
db: DB,
executor: (utils: typeof genericSqliteUtils) => Promisable<IGenericSqlite<DB>>,
executor: (utils: typeof genericSqliteUtils) => Promisable<IGenericCustomSqlite<DB>>,
config?: GenericSqliteConnectionConfig,
) {
return new GenericSqliteConnection(db, () => executor(genericSqliteUtils), {

View File

@@ -1,12 +0,0 @@
import { connectionTestSuite } from "../connection-test-suite";
import { LibsqlConnection } from "./LibsqlConnection";
import { bunTestRunner } from "adapter/bun/test";
import { describe } from "bun:test";
import { createClient } from "@libsql/client";
describe("LibsqlConnection", () => {
connectionTestSuite(bunTestRunner, {
makeConnection: () => new LibsqlConnection(createClient({ url: ":memory:" })),
rawDialectDetails: ["rowsAffected", "lastInsertRowid"],
});
});

View File

@@ -1,62 +0,0 @@
import type { Client, Config, InStatement } from "@libsql/client";
import { createClient } from "libsql-stateless-easy";
import { LibsqlDialect } from "@libsql/kysely-libsql";
import { FilterNumericKeysPlugin } from "data/plugins/FilterNumericKeysPlugin";
import { type ConnQuery, type ConnQueryResults, SqliteConnection } from "bknd/data";
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
export type LibSqlCredentials = Config & {
protocol?: (typeof LIBSQL_PROTOCOLS)[number];
};
function getClient(clientOrCredentials: Client | LibSqlCredentials): Client {
if (clientOrCredentials && "url" in clientOrCredentials) {
let { url, authToken, protocol } = clientOrCredentials;
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
console.info("changing protocol to", protocol);
const [, rest] = url.split("://");
url = `${protocol}://${rest}`;
}
return createClient({ url, authToken });
}
return clientOrCredentials as Client;
}
export class LibsqlConnection extends SqliteConnection<Client> {
override name = "libsql";
protected override readonly supported = {
batching: true,
softscans: true,
};
constructor(clientOrCredentials: Client | LibSqlCredentials) {
const client = getClient(clientOrCredentials);
super({
excludeTables: ["libsql_wasm_func_table"],
dialect: LibsqlDialect,
dialectArgs: [{ client }],
additionalPlugins: [new FilterNumericKeysPlugin()],
});
this.client = client;
}
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
const compiled = this.getCompiled(...qbs);
const stms: InStatement[] = compiled.map((q) => {
return {
sql: q.sql,
args: q.parameters as any[],
};
});
return this.withTransformedRows(await this.client.batch(stms)) as any;
}
}
export function libsql(credentials: Client | LibSqlCredentials): LibsqlConnection {
return new LibsqlConnection(credentials);
}

View File

@@ -68,32 +68,34 @@ export class SqliteIntrospector extends BaseIntrospector {
return tables.map((table) => ({
name: table.name,
isView: table.type === "view",
columns: table.columns.map((col) => {
const autoIncrementCol = table.sql
?.split(/[\(\),]/)
?.find((it) => it.toLowerCase().includes("autoincrement"))
?.trimStart()
?.split(/\s+/)?.[0]
?.replace(/["`]/g, "");
columns:
table.columns?.map((col) => {
const autoIncrementCol = table.sql
?.split(/[\(\),]/)
?.find((it) => it.toLowerCase().includes("autoincrement"))
?.trimStart()
?.split(/\s+/)?.[0]
?.replace(/["`]/g, "");
return {
name: col.name,
dataType: col.type,
isNullable: !col.notnull,
isAutoIncrementing: col.name === autoIncrementCol,
hasDefaultValue: col.dflt_value != null,
comment: undefined,
};
}),
indices: table.indices.map((index) => ({
name: index.name,
table: table.name,
isUnique: index.sql?.match(/unique/i) != null,
columns: index.columns.map((col) => ({
name: col.name,
order: col.seqno,
})),
})),
return {
name: col.name,
dataType: col.type,
isNullable: !col.notnull,
isAutoIncrementing: col.name === autoIncrementCol,
hasDefaultValue: col.dflt_value != null,
comment: undefined,
};
}) ?? [],
indices:
table.indices?.map((index) => ({
name: index.name,
table: table.name,
isUnique: index.sql?.match(/unique/i) != null,
columns: index.columns.map((col) => ({
name: col.name,
order: col.seqno,
})),
})) ?? [],
}));
}
}

View File

@@ -0,0 +1,15 @@
import { connectionTestSuite } from "../../connection-test-suite";
import { libsql } from "./LibsqlConnection";
import { bunTestRunner } from "adapter/bun/test";
import { describe } from "bun:test";
import { createClient } from "@libsql/client";
describe("LibsqlConnection", () => {
connectionTestSuite(bunTestRunner, {
makeConnection: () => ({
connection: libsql(createClient({ url: ":memory:" })),
dispose: async () => {},
}),
rawDialectDetails: [],
});
});

View File

@@ -0,0 +1,68 @@
import type { Client, Config, ResultSet } from "@libsql/client";
import { createClient } from "libsql-stateless-easy";
import { FilterNumericKeysPlugin } from "data/plugins/FilterNumericKeysPlugin";
import {
genericSqlite,
type GenericSqliteConnection,
} from "data/connection/sqlite/GenericSqliteConnection";
import type { QueryResult } from "kysely";
export type LibsqlConnection = GenericSqliteConnection<Client>;
export type LibSqlCredentials = Config;
function getClient(clientOrCredentials: Client | LibSqlCredentials): Client {
if (clientOrCredentials && "url" in clientOrCredentials) {
const { url, authToken } = clientOrCredentials;
return createClient({ url, authToken });
}
return clientOrCredentials as Client;
}
export function libsql(config: LibSqlCredentials | Client) {
const db = getClient(config);
return genericSqlite(
"libsql",
db,
(utils) => {
const mapResult = (result: ResultSet): QueryResult<any> => ({
insertId: result.lastInsertRowid,
numAffectedRows: BigInt(result.rowsAffected),
rows: result.rows,
});
const execute = async (sql: string, parameters?: any[] | readonly any[]) => {
const result = await db.execute({ sql, args: [...(parameters || [])] });
return mapResult(result);
};
return {
db,
batch: async (stmts) => {
const results = await db.batch(
stmts.map(({ sql, parameters }) => ({
sql,
args: parameters as any[],
})),
);
return results.map(mapResult);
},
query: utils.buildQueryFn({
all: async (sql, parameters) => {
return (await execute(sql, parameters)).rows;
},
run: execute,
}),
close: () => db.close(),
};
},
{
supports: {
batching: true,
softscans: true,
},
additionalPlugins: [new FilterNumericKeysPlugin()],
excludeTables: ["libsql_wasm_func_table"],
},
);
}

View File

@@ -1,5 +1,6 @@
import { $console, config } from "core";
import { config } from "core";
import {
$console,
type Static,
StringEnum,
parse,

View File

@@ -1,4 +1,5 @@
import { $console, type DB as DefaultDB } from "core";
import type { DB as DefaultDB } from "core";
import { $console } from "core/utils";
import { EventManager } from "core/events";
import { sql } from "kysely";
import { Connection } from "../connection/Connection";

View File

@@ -1,4 +1,4 @@
import { $console } from "core/console";
import { $console } from "core/utils";
import type { Entity, EntityData } from "../Entity";
import type { EntityManager } from "../EntityManager";
import { Result, type ResultJSON, type ResultOptions } from "../Result";
@@ -32,6 +32,7 @@ export class MutatorResult<T = EntityData[]> extends Result<T> {
onError: (error) => {
if (!options?.silent) {
$console.error("[ERROR] Mutator:", error.message);
throw error;
}
},
...options,

View File

@@ -1,5 +1,5 @@
import type { DB as DefaultDB, PrimaryFieldType } from "core";
import { $console } from "core";
import { $console } from "core/utils";
import { type EmitsEvents, EventManager } from "core/events";
import { type SelectQueryBuilder, sql } from "kysely";
import { InvalidSearchParamsException } from "../../errors";
@@ -57,7 +57,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
}
}
getValidOptions(options?: RepoQuery): RepoQuery {
getValidOptions(options?: Partial<RepoQuery>): RepoQuery {
const entity = this.entity;
// @todo: if not cloned deep, it will keep references and error if multiple requests come in
const validated = {

View File

@@ -1,9 +1,8 @@
import { $console } from "core/console";
import type { Entity, EntityData } from "../Entity";
import type { EntityManager } from "../EntityManager";
import { Result, type ResultJSON, type ResultOptions } from "../Result";
import type { Compilable, SelectQueryBuilder } from "kysely";
import { ensureInt } from "core/utils";
import { $console, ensureInt } from "core/utils";
export type RepositoryResultOptions = ResultOptions & {
silent?: boolean;

View File

@@ -1,4 +1,5 @@
import { $console, type PrimaryFieldType } from "core";
import type { PrimaryFieldType } from "core";
import { $console } from "core/utils";
import { Event, InvalidEventReturn } from "core/events";
import type { Entity, EntityData } from "../entities";
import type { RepoQuery } from "data/server/query";

View File

@@ -1,7 +1,6 @@
import { type Static, StringEnum, dayjs } from "core/utils";
import { $console, type Static, StringEnum, dayjs } from "core/utils";
import type { EntityManager } from "../entities";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
import { $console } from "core";
import * as tbbox from "@sinclair/typebox";
import type { TFieldTSType } from "data/entities/EntityTypescript";
const { Type } = tbbox;

View File

@@ -30,7 +30,7 @@ export * as DataPermissions from "./permissions";
export { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField";
export { libsql } from "./connection/sqlite/LibsqlConnection";
export { libsql } from "./connection/sqlite/libsql/LibsqlConnection";
export {
genericSqlite,
genericSqliteUtils,

View File

@@ -2,7 +2,7 @@ import type { CompiledQuery, TableMetadata } from "kysely";
import type { IndexMetadata, SchemaResponse } from "../connection/Connection";
import type { Entity, EntityManager } from "../entities";
import { PrimaryField } from "../fields";
import { $console } from "core";
import { $console } from "core/utils";
type IntrospectedTable = TableMetadata & {
indices: IndexMetadata[];

View File

@@ -1,7 +1,6 @@
import { s } from "core/object/schema";
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
import { $console } from "core";
import { isObject } from "core/utils";
import { isObject, $console } from "core/utils";
import type { CoercionOptions, TAnyOf } from "jsonv-ts";
// -------
@@ -150,4 +149,6 @@ export type RepoQueryIn = {
join?: string[];
where?: WhereQuery;
};
export type RepoQuery = s.StaticCoerced<typeof repoQuery>;
export type RepoQuery = s.StaticCoerced<typeof repoQuery> & {
sort: SortSchema;
};

View File

@@ -2,7 +2,7 @@ import { Event, EventManager, type ListenerHandler } from "core/events";
import type { EmitsEvents } from "core/events";
import type { Task, TaskResult } from "../tasks/Task";
import type { Flow } from "./Flow";
import { $console } from "core";
import { $console } from "core/utils";
export type TaskLog = TaskResult & {
task: Task;

View File

@@ -1,11 +1,10 @@
import { objectTransform, transformObject } from "core/utils";
import { $console, transformObject } from "core/utils";
import { type TaskMapType, TriggerMap } from "../index";
import type { Task } from "../tasks/Task";
import { Condition, TaskConnection } from "../tasks/TaskConnection";
import { Execution } from "./Execution";
import { FlowTaskConnector } from "./FlowTaskConnector";
import { Trigger } from "./triggers/Trigger";
import { $console } from "core";
type Jsoned<T extends { toJSON: () => object }> = ReturnType<T["toJSON"]>;

View File

@@ -1,5 +1,5 @@
import type { Task } from "../../tasks/Task";
import { $console } from "core";
import { $console } from "core/utils";
export class RuntimeExecutor {
async run(

View File

@@ -1,7 +1,7 @@
import type { EventManager } from "core/events";
import type { Flow } from "../Flow";
import { Trigger } from "./Trigger";
import { $console } from "core";
import { $console } from "core/utils";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;

View File

@@ -1,5 +1,5 @@
import { Task } from "../Task";
import { $console } from "core";
import { $console } from "core/utils";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;

View File

@@ -16,6 +16,7 @@ export {
type ModuleManagerOptions,
type ModuleBuildContext,
type InitialModuleConfigs,
ModuleManagerEvents,
} from "./modules/ModuleManager";
export type { ServerEnv } from "modules/Controller";

View File

@@ -1,4 +1,5 @@
import { $console, type AppEntity } from "core";
import type { AppEntity } from "core";
import { $console } from "core/utils";
import type { Entity, EntityManager } from "data";
import { type FileUploadedEventData, Storage, type StorageAdapter, MediaPermissions } from "media";
import { Module } from "modules/Module";

View File

@@ -1,9 +1,8 @@
import { type EmitsEvents, EventManager } from "core/events";
import { isFile, detectImageDimensions } from "core/utils";
import { $console, isFile, detectImageDimensions } from "core/utils";
import { isMimeType } from "media/storage/mime-types-tiny";
import * as StorageEvents from "./events";
import type { FileUploadedEventData } from "./events";
import { $console } from "core";
import type { StorageAdapter } from "./StorageAdapter";
export type FileListObject = {

View File

@@ -1,4 +1,5 @@
import { $console, type PrimaryFieldType } from "core";
import type { PrimaryFieldType } from "core";
import { $console } from "core/utils";
import { isDebug } from "core/env";
import { encodeSearch } from "core/utils/reqres";
import type { ApiFetcher } from "Api";

View File

@@ -1,6 +1,7 @@
import { Guard } from "auth";
import { $console, BkndError, DebugLogger, env } from "core";
import { EventManager } from "core/events";
import { BkndError, DebugLogger, env } from "core";
import { $console } from "core/utils";
import { EventManager, Event } from "core/events";
import * as $diff from "core/object/diff";
import {
Default,
@@ -126,9 +127,24 @@ interface T_INTERNAL_EM {
const debug_modules = env("modules_debug");
abstract class ModuleManagerEvent<A = {}> extends Event<{ ctx: ModuleBuildContext } & A> {}
export class ModuleManagerConfigUpdateEvent<
Module extends keyof ModuleConfigs,
> extends ModuleManagerEvent<{
module: Module;
config: ModuleConfigs[Module];
}> {
static override slug = "mm-config-update";
}
export const ModuleManagerEvents = {
ModuleManagerConfigUpdateEvent,
};
// @todo: cleanup old diffs on upgrade
// @todo: cleanup multiple backups on upgrade
export class ModuleManager {
static Events = ModuleManagerEvents;
protected modules: Modules;
// internal em for __bknd config table
__em!: EntityManager<T_INTERNAL_EM>;
@@ -151,7 +167,7 @@ export class ModuleManager {
) {
this.__em = new EntityManager([__bknd], this.connection);
this.modules = {} as Modules;
this.emgr = new EventManager();
this.emgr = new EventManager({ ...ModuleManagerEvents });
this.logger = new DebugLogger(debug_modules);
let initial = {} as Partial<ModuleConfigs>;
@@ -628,6 +644,13 @@ export class ModuleManager {
try {
// overwrite listener to run build inside this try/catch
module.setListener(async () => {
await this.emgr.emit(
new ModuleManagerConfigUpdateEvent({
ctx: this.ctx(),
module: name,
config: module.config as any,
}),
);
await this.buildModules();
});

View File

@@ -1,7 +1,8 @@
/** @jsxImportSource hono/jsx */
import type { App } from "App";
import { $console, config, isDebug } from "core";
import { config, isDebug } from "core";
import { $console } from "core/utils";
import { addFlashMessage } from "core/server/flash";
import { html } from "hono/html";
import { Fragment } from "hono/jsx";

View File

@@ -1,5 +1,5 @@
import { Exception, isDebug, $console } from "core";
import { type Static, StringEnum } from "core/utils";
import { Exception, isDebug } from "core";
import { type Static, StringEnum, $console } from "core/utils";
import { cors } from "hono/cors";
import { Module } from "modules/Module";
import * as tbbox from "@sinclair/typebox";

View File

@@ -1,9 +1,8 @@
/// <reference types="@cloudflare/workers-types" />
import type { App } from "App";
import { $console, tbValidator as tb } from "core";
import {
StringEnum,
$console,
TypeInvalidError,
datetimeStringLocal,
datetimeStringUTC,

View File

@@ -6,8 +6,8 @@ import { StorageLocalAdapter } from "./src/adapter/node";
import type { Connection } from "./src/data/connection/Connection";
import { __bknd } from "modules/ModuleManager";
import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection";
import { libsql } from "./src/data/connection/sqlite/LibsqlConnection";
import { $console } from "core";
import { libsql } from "./src/data/connection/sqlite/libsql/LibsqlConnection";
import { $console } from "core/utils";
import { createClient } from "@libsql/client";
registries.media.register("local", StorageLocalAdapter);

View File

@@ -15,7 +15,7 @@
},
"app": {
"name": "bknd",
"version": "0.15.0-rc.2",
"version": "0.15.0-rc.3",
"bin": "./dist/cli/index.js",
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
@@ -37,13 +37,13 @@
"json-schema-form-react": "^0.0.2",
"json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1",
"jsonv-ts": "^0.1.0",
"kysely": "^0.27.6",
"lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2",
"radix-ui": "^1.1.3",
"swr": "^2.3.3",
"uuid": "^11.1.0",
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.758.0",
@@ -55,7 +55,6 @@
"@hono/vite-dev-server": "^0.19.1",
"@hookform/resolvers": "^4.1.3",
"@libsql/client": "^0.15.9",
"@libsql/kysely-libsql": "^0.4.1",
"@mantine/modals": "^7.17.1",
"@mantine/notifications": "^7.17.1",
"@playwright/test": "^1.51.1",
@@ -75,7 +74,6 @@
"dotenv": "^16.4.7",
"jotai": "^2.12.2",
"jsdom": "^26.0.0",
"jsonv-ts": "^0.1.0",
"kysely-d1": "^0.3.0",
"kysely-generic-sqlite": "^1.2.1",
"libsql-stateless-easy": "^1.8.0",
@@ -98,6 +96,7 @@
"tsc-alias": "^1.8.11",
"tsup": "^8.4.0",
"tsx": "^4.19.3",
"uuid": "^11.1.0",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9",
@@ -754,8 +753,6 @@
"@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="],
"@libsql/kysely-libsql": ["@libsql/kysely-libsql@0.4.1", "", { "dependencies": { "@libsql/client": "^0.8.0" }, "peerDependencies": { "kysely": "*" } }, "sha512-mCTa6OWgoME8LNu22COM6XjKBmcMAvNtIO6DYM10jSAFq779fVlrTKQEmXIB8TwJVU65dA5jGCpT8gkDdWS0HQ=="],
"@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.13", "", { "os": "linux", "cpu": "arm" }, "sha512-UEW+VZN2r0mFkfztKOS7cqfS8IemuekbjUXbXCwULHtusww2QNCXvM5KU9eJCNE419SZCb0qaEWYytcfka8qeA=="],
"@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.13", "", { "os": "linux", "cpu": "arm" }, "sha512-NMDgLqryYBv4Sr3WoO/m++XDjR5KLlw9r/JK4Ym6A1XBv2bxQQNhH0Lxx3bjLW8qqhBD4+0xfms4d2cOlexPyA=="],
@@ -1222,7 +1219,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="],
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@@ -3878,8 +3875,6 @@
"@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"@libsql/kysely-libsql/@libsql/client": ["@libsql/client@0.8.1", "", { "dependencies": { "@libsql/core": "^0.8.1", "@libsql/hrana-client": "^0.6.2", "js-base64": "^3.7.5", "libsql": "^0.3.10", "promise-limit": "^2.7.0" } }, "sha512-xGg0F4iTDFpeBZ0r4pA6icGsYa5rG6RAG+i/iLDnpCAnSuTqEWMDdPlVseiq4Z/91lWI9jvvKKiKpovqJ1kZWA=="],
"@neondatabase/serverless/@types/pg": ["@types/pg@8.6.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw=="],
"@plasmicapp/query/swr": ["swr@1.3.0", "", { "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0" } }, "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw=="],
@@ -4020,7 +4015,7 @@
"@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
"@types/bun/bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
"@types/bun/bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"@types/pg/pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
@@ -4630,12 +4625,6 @@
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"@libsql/kysely-libsql/@libsql/client/@libsql/core": ["@libsql/core@0.8.1", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-u6nrj6HZMTPsgJ9EBhLzO2uhqhlHQJQmVHV+0yFLvfGf3oSP8w7TjZCNUgu1G8jHISx6KFi7bmcrdXW9lRt++A=="],
"@libsql/kysely-libsql/@libsql/client/@libsql/hrana-client": ["@libsql/hrana-client@0.6.2", "", { "dependencies": { "@libsql/isomorphic-fetch": "^0.2.1", "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-MWxgD7mXLNf9FXXiM0bc90wCjZSpErWKr5mGza7ERy2FJNNMXd7JIOv+DepBA1FQTIfI8TFO4/QDYgaQC0goNw=="],
"@libsql/kysely-libsql/@libsql/client/libsql": ["libsql@0.3.19", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2", "libsql": "^0.3.15" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.3.19", "@libsql/darwin-x64": "0.3.19", "@libsql/linux-arm64-gnu": "0.3.19", "@libsql/linux-arm64-musl": "0.3.19", "@libsql/linux-x64-gnu": "0.3.19", "@libsql/linux-x64-musl": "0.3.19", "@libsql/win32-x64-msvc": "0.3.19" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-Aj5cQ5uk/6fHdmeW0TiXK42FqUlwx7ytmMLPSaUQPin5HKKKuUPD62MAbN4OEweGBBI7q1BekoEN4gPUEL6MZA=="],
"@testing-library/dom/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
@@ -4986,24 +4975,6 @@
"@cloudflare/vitest-pool-workers/miniflare/youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"@libsql/kysely-libsql/@libsql/client/@libsql/hrana-client/@libsql/isomorphic-fetch": ["@libsql/isomorphic-fetch@0.2.5", "", {}, "sha512-8s/B2TClEHms2yb+JGpsVRTPBfy1ih/Pq6h6gvyaNcYnMVJvgQRY7wAa8U2nD0dppbCuDU5evTNMEhrQ17ZKKg=="],
"@libsql/kysely-libsql/@libsql/client/@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"@libsql/kysely-libsql/@libsql/client/libsql/@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.3.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rmOqsLcDI65zzxlUOoEiPJLhqmbFsZF6p4UJQ2kMqB+Kc0Rt5/A1OAdOZ/Wo8fQfJWjR1IbkbpEINFioyKf+nQ=="],
"@libsql/kysely-libsql/@libsql/client/libsql/@libsql/darwin-x64": ["@libsql/darwin-x64@0.3.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-q9O55B646zU+644SMmOQL3FIfpmEvdWpRpzubwFc2trsa+zoBlSkHuzU9v/C+UNoPHQVRMP7KQctJ455I/h/xw=="],
"@libsql/kysely-libsql/@libsql/client/libsql/@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.3.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-mgeAUU1oqqh57k7I3cQyU6Trpdsdt607eFyEmH5QO7dv303ti+LjUvh1pp21QWV6WX7wZyjeJV1/VzEImB+jRg=="],
"@libsql/kysely-libsql/@libsql/client/libsql/@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.3.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-VEZtxghyK6zwGzU9PHohvNxthruSxBEnRrX7BSL5jQ62tN4n2JNepJ6SdzXp70pdzTfwroOj/eMwiPt94gkVRg=="],
"@libsql/kysely-libsql/@libsql/client/libsql/@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.3.19", "", { "os": "linux", "cpu": "x64" }, "sha512-2t/J7LD5w2f63wGihEO+0GxfTyYIyLGEvTFEsMO16XI5o7IS9vcSHrxsvAJs4w2Pf907uDjmc7fUfMg6L82BrQ=="],
"@libsql/kysely-libsql/@libsql/client/libsql/@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.3.19", "", { "os": "linux", "cpu": "x64" }, "sha512-BLsXyJaL8gZD8+3W2LU08lDEd9MIgGds0yPy5iNPp8tfhXx3pV/Fge2GErN0FC+nzt4DYQtjL+A9GUMglQefXQ=="],
"@libsql/kysely-libsql/@libsql/client/libsql/@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.3.19", "", { "os": "win32", "cpu": "x64" }, "sha512-ay1X9AobE4BpzG0XPw1gplyLZPGHIgJOovvW23gUrukRegiUP62uzhpRbKNogLlUOynyXeq//prHgPXiebUfWg=="],
"@verdaccio/middleware/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"@vitest/coverage-v8/vitest/vite/rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="],