diff --git a/app/build.ts b/app/build.ts index 49bdf95..824f006 100644 --- a/app/build.ts +++ b/app/build.ts @@ -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 diff --git a/app/package.json b/app/package.json index 0fc8819..4748fb6 100644 --- a/app/package.json +++ b/app/package.json @@ -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": { diff --git a/app/src/App.ts b/app/src/App.ts index 223711a..f1a495b 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -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 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 { connectionTestSuite(bunTestRunner, { - makeConnection: () => bunSqlite({ database: new Database(":memory:") }), + makeConnection: () => ({ + connection: bunSqlite({ database: new Database(":memory:") }), + dispose: async () => {}, + }), rawDialectDetails: [], }); }); diff --git a/app/src/adapter/bun/test.ts b/app/src/adapter/bun/test.ts index 1cb6ca5..4d453d7 100644 --- a/app/src/adapter/bun/test.ts +++ b/app/src/adapter/bun/test.ts @@ -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, }; diff --git a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts index c78eb92..d1b6712 100644 --- a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts +++ b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts @@ -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 { diff --git a/app/src/adapter/cloudflare/config.ts b/app/src/adapter/cloudflare/config.ts index 33a81e0..7f3eaee 100644 --- a/app/src/adapter/cloudflare/config.ts +++ b/app/src/adapter/cloudflare/config.ts @@ -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( // 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( 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( }, }; } 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"); } diff --git a/app/src/adapter/cloudflare/connection/D1Connection.ts b/app/src/adapter/cloudflare/connection/D1Connection.ts index d7eb5a0..b2344e1 100644 --- a/app/src/adapter/cloudflare/connection/D1Connection.ts +++ b/app/src/adapter/cloudflare/connection/D1Connection.ts @@ -1,42 +1,78 @@ /// -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; export type D1ConnectionConfig = { binding: DB; }; -export class D1Connection< - DB extends D1Database | D1DatabaseSession = D1Database, -> extends SqliteConnection { - override name = "sqlite-d1"; +export function d1Sqlite( + config: D1ConnectionConfig, +) { + 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) { - super({ + const mapResult = (res: D1Result): QueryResult => { + 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(...qbs: O): Promise> { - 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; - } + }, + ); } diff --git a/app/src/adapter/cloudflare/connection/D1Connection.vitest.ts b/app/src/adapter/cloudflare/connection/D1Connection.vitest.ts new file mode 100644 index 0000000..a535e48 --- /dev/null +++ b/app/src/adapter/cloudflare/connection/D1Connection.vitest.ts @@ -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", + ], + }); +}); diff --git a/app/src/adapter/cloudflare/connection/DoConnection.ts b/app/src/adapter/cloudflare/connection/DoConnection.ts new file mode 100644 index 0000000..c7c8500 --- /dev/null +++ b/app/src/adapter/cloudflare/connection/DoConnection.ts @@ -0,0 +1,83 @@ +/// + +import { + genericSqlite, + type GenericSqliteConnection, +} from "data/connection/sqlite/GenericSqliteConnection"; +import type { QueryResult } from "kysely"; + +export type D1SqliteConnection = GenericSqliteConnection; +export type DurableObjecSql = DurableObjectState["storage"]["sql"]; + +export type D1ConnectionConfig = + | DurableObjectState + | { + sql: DB; + }; + +export function doSqlite(config: D1ConnectionConfig) { + 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>, + ): QueryResult => { + 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"], + }, + ); +} diff --git a/app/src/adapter/cloudflare/connection/DoConnection.vitest.ts b/app/src/adapter/cloudflare/connection/DoConnection.vitest.ts new file mode 100644 index 0000000..695046e --- /dev/null +++ b/app/src/adapter/cloudflare/connection/DoConnection.vitest.ts @@ -0,0 +1,92 @@ +/// + +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; + } + >; + + 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"], + }); +}); diff --git a/app/src/adapter/cloudflare/index.ts b/app/src/adapter/cloudflare/index.ts index 60e6a77..26044ba 100644 --- a/app/src/adapter/cloudflare/index.ts +++ b/app/src/adapter/cloudflare/index.ts @@ -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( + config: D1ConnectionConfig, +) { + return d1Sqlite(config); } diff --git a/app/src/adapter/cloudflare/modes/durable.ts b/app/src/adapter/cloudflare/modes/durable.ts index 25dd0b4..4812b0c 100644 --- a/app/src/adapter/cloudflare/modes/durable.ts +++ b/app/src/adapter/cloudflare/modes/durable.ts @@ -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( config: CloudflareBkndConfig, diff --git a/app/src/adapter/index.ts b/app/src/adapter/index.ts index 7554040..9e74dd0 100644 --- a/app/src/adapter/index.ts +++ b/app/src/adapter/index.ts @@ -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"; diff --git a/app/src/adapter/node/connection/NodeSqliteConnection.ts b/app/src/adapter/node/connection/NodeSqliteConnection.ts index 60ad9df..fb298fa 100644 --- a/app/src/adapter/node/connection/NodeSqliteConnection.ts +++ b/app/src/adapter/node/connection/NodeSqliteConnection.ts @@ -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, }, - }; - }); + }, + ); } diff --git a/app/src/adapter/node/connection/NodeSqliteConnection.vi-test.ts b/app/src/adapter/node/connection/NodeSqliteConnection.vi-test.ts index 62ee9cb..2cb9149 100644 --- a/app/src/adapter/node/connection/NodeSqliteConnection.vi-test.ts +++ b/app/src/adapter/node/connection/NodeSqliteConnection.vi-test.ts @@ -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: [], }); }); diff --git a/app/src/adapter/node/node.adapter.ts b/app/src/adapter/node/node.adapter.ts index 9d9d5d5..88b7d62 100644 --- a/app/src/adapter/node/node.adapter.ts +++ b/app/src/adapter/node/node.adapter.ts @@ -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; diff --git a/app/src/adapter/node/test.ts b/app/src/adapter/node/test.ts index 5a634ae..3c78f25 100644 --- a/app/src/adapter/node/test.ts +++ b/app/src/adapter/node/test.ts @@ -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: () => {}, }; diff --git a/app/src/adapter/node/vitest.ts b/app/src/adapter/node/vitest.ts index 569be7a..8f6988e 100644 --- a/app/src/adapter/node/vitest.ts +++ b/app/src/adapter/node/vitest.ts @@ -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, }; diff --git a/app/src/adapter/sqlite/edge.ts b/app/src/adapter/sqlite/edge.ts index 97b09ec..f1de953 100644 --- a/app/src/adapter/sqlite/edge.ts +++ b/app/src/adapter/sqlite/edge.ts @@ -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); diff --git a/app/src/adapter/sqlite/types.ts b/app/src/adapter/sqlite/types.ts deleted file mode 100644 index 2a0a5e5..0000000 --- a/app/src/adapter/sqlite/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { Connection } from "bknd/data"; - -export type SqliteConnection = (config: { url: string }) => Connection; diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index 973332e..ac78b9f 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -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"; diff --git a/app/src/auth/AppUserPool.ts b/app/src/auth/AppUserPool.ts index 23f24d0..128de6c 100644 --- a/app/src/auth/AppUserPool.ts +++ b/app/src/auth/AppUserPool.ts @@ -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, diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 4357da0..29b6597 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -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, diff --git a/app/src/auth/authenticate/strategies/PasswordStrategy.ts b/app/src/auth/authenticate/strategies/PasswordStrategy.ts index 706e14b..6bf059e 100644 --- a/app/src/auth/authenticate/strategies/PasswordStrategy.ts +++ b/app/src/auth/authenticate/strategies/PasswordStrategy.ts @@ -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"; diff --git a/app/src/auth/authorize/Guard.ts b/app/src/auth/authorize/Guard.ts index a45c160..81280db 100644 --- a/app/src/auth/authorize/Guard.ts +++ b/app/src/auth/authorize/Guard.ts @@ -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"; diff --git a/app/src/auth/middlewares.ts b/app/src/auth/middlewares.ts index d6f28c0..b58b540 100644 --- a/app/src/auth/middlewares.ts +++ b/app/src/auth/middlewares.ts @@ -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"; diff --git a/app/src/cli/commands/run/platform.ts b/app/src/cli/commands/run/platform.ts index c3a4110..bc3379b 100644 --- a/app/src/cli/commands/run/platform.ts +++ b/app/src/cli/commands/run/platform.ts @@ -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"; diff --git a/app/src/cli/commands/run/run.ts b/app/src/cli/commands/run/run.ts index 7ed5bed..0830bc6 100644 --- a/app/src/cli/commands/run/run.ts +++ b/app/src/cli/commands/run/run.ts @@ -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({ diff --git a/app/src/cli/commands/user.ts b/app/src/cli/commands/user.ts index 0a85d9b..4f4db7c 100644 --- a/app/src/cli/commands/user.ts +++ b/app/src/cli/commands/user.ts @@ -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"; diff --git a/app/src/cli/utils/sys.ts b/app/src/cli/utils/sys.ts index b4536a5..56ae32e 100644 --- a/app/src/cli/utils/sys.ts +++ b/app/src/cli/utils/sys.ts @@ -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"; diff --git a/app/src/cli/utils/telemetry.ts b/app/src/cli/utils/telemetry.ts index 9fddb42..6673e0e 100644 --- a/app/src/cli/utils/telemetry.ts +++ b/app/src/cli/utils/telemetry.ts @@ -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 }; diff --git a/app/src/core/events/EventManager.ts b/app/src/core/events/EventManager.ts index 59efc8f..78db931 100644 --- a/app/src/core/events/EventManager.ts +++ b/app/src/core/events/EventManager.ts @@ -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 diff --git a/app/src/core/index.ts b/app/src/core/index.ts index a0e96e6..ad4b1a8 100644 --- a/app/src/core/index.ts +++ b/app/src/core/index.ts @@ -38,7 +38,6 @@ export { } from "./object/schema"; export * from "./drivers"; -export * from "./console"; export * from "./events"; // compatibility diff --git a/app/src/core/test/index.ts b/app/src/core/test/index.ts index c731938..4e9bfef 100644 --- a/app/src/core/test/index.ts +++ b/app/src/core/test/index.ts @@ -1,3 +1,5 @@ +import type { MaybePromise } from "core/types"; + export type Matcher = { 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; + describe: (label: string, asyncFn: () => MaybePromise) => void; test: Test; mock: any>(fn: T) => T | any; expect: ( @@ -26,6 +28,9 @@ export type TestRunner = { resolves: Matcher>; rejects: Matcher>; }; + beforeEach: (fn: () => MaybePromise) => void; + afterEach: (fn: () => MaybePromise) => void; + afterAll: (fn: () => MaybePromise) => void; }; export async function retry( diff --git a/app/src/core/console.ts b/app/src/core/utils/console.ts similarity index 100% rename from app/src/core/console.ts rename to app/src/core/utils/console.ts diff --git a/app/src/core/utils/file.ts b/app/src/core/utils/file.ts index 152c71d..ea5eb2b 100644 --- a/app/src/core/utils/file.ts +++ b/app/src/core/utils/file.ts @@ -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; diff --git a/app/src/core/utils/index.ts b/app/src/core/utils/index.ts index c94c4bb..19bcef6 100644 --- a/app/src/core/utils/index.ts +++ b/app/src/core/utils/index.ts @@ -1,3 +1,4 @@ +export * from "./console"; export * from "./browser"; export * from "./objects"; export * from "./strings"; diff --git a/app/src/core/utils/test.ts b/app/src/core/utils/test.ts index 91ae2d3..44d38d9 100644 --- a/app/src/core/utils/test.ts +++ b/app/src/core/utils/test.ts @@ -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() { diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index f7db2b9..6d6acf4 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -1,4 +1,3 @@ -import { $console, isDebug } from "core"; import { DataPermissions, type EntityData, diff --git a/app/src/data/connection/connection-test-suite.ts b/app/src/data/connection/connection-test-suite.ts index 59bba0e..af1eeba 100644 --- a/app/src/data/connection/connection-test-suite.ts +++ b/app/src/data/connection/connection-test-suite.ts @@ -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; + }>; 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>; + 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>; + 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); + }); + }); } diff --git a/app/src/data/connection/sqlite/GenericSqliteConnection.ts b/app/src/data/connection/sqlite/GenericSqliteConnection.ts index 5bd4543..98a584b 100644 --- a/app/src/data/connection/sqlite/GenericSqliteConnection.ts +++ b/app/src/data/connection/sqlite/GenericSqliteConnection.ts @@ -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 extends IGenericSqlite { + batch?: (stmts: TStatement[]) => Promisable[]>; +} + export type GenericSqliteConnectionConfig = { - name: string; + name?: string; additionalPlugins?: KyselyPlugin[]; excludeTables?: string[]; onCreateConnection?: OnCreateConnection; @@ -21,10 +26,11 @@ export type GenericSqliteConnectionConfig = { export class GenericSqliteConnection extends SqliteConnection { override name = "generic-sqlite"; + #executor: IGenericCustomSqlite | undefined; constructor( - db: DB, - executor: () => Promisable, + public db: DB, + private executor: () => Promisable>, config?: GenericSqliteConnectionConfig, ) { super({ @@ -39,18 +45,43 @@ export class GenericSqliteConnection extends SqliteConnection } 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(...qbs: O): Promise> { + 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( name: string, db: DB, - executor: (utils: typeof genericSqliteUtils) => Promisable>, + executor: (utils: typeof genericSqliteUtils) => Promisable>, config?: GenericSqliteConnectionConfig, ) { return new GenericSqliteConnection(db, () => executor(genericSqliteUtils), { diff --git a/app/src/data/connection/sqlite/LibsqlConnection.spec.ts b/app/src/data/connection/sqlite/LibsqlConnection.spec.ts deleted file mode 100644 index d6f14d1..0000000 --- a/app/src/data/connection/sqlite/LibsqlConnection.spec.ts +++ /dev/null @@ -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"], - }); -}); diff --git a/app/src/data/connection/sqlite/LibsqlConnection.ts b/app/src/data/connection/sqlite/LibsqlConnection.ts deleted file mode 100644 index cf68962..0000000 --- a/app/src/data/connection/sqlite/LibsqlConnection.ts +++ /dev/null @@ -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 { - 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(...qbs: O): Promise> { - 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); -} diff --git a/app/src/data/connection/sqlite/SqliteIntrospector.ts b/app/src/data/connection/sqlite/SqliteIntrospector.ts index 5821bc1..70c3ff6 100644 --- a/app/src/data/connection/sqlite/SqliteIntrospector.ts +++ b/app/src/data/connection/sqlite/SqliteIntrospector.ts @@ -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, + })), + })) ?? [], })); } } diff --git a/app/src/data/connection/sqlite/libsql/LibsqlConnection.spec.ts b/app/src/data/connection/sqlite/libsql/LibsqlConnection.spec.ts new file mode 100644 index 0000000..7ae691f --- /dev/null +++ b/app/src/data/connection/sqlite/libsql/LibsqlConnection.spec.ts @@ -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: [], + }); +}); diff --git a/app/src/data/connection/sqlite/libsql/LibsqlConnection.ts b/app/src/data/connection/sqlite/libsql/LibsqlConnection.ts new file mode 100644 index 0000000..c99ad59 --- /dev/null +++ b/app/src/data/connection/sqlite/libsql/LibsqlConnection.ts @@ -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; +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 => ({ + 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"], + }, + ); +} diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index 48f23b9..e0eb12c 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -1,5 +1,6 @@ -import { $console, config } from "core"; +import { config } from "core"; import { + $console, type Static, StringEnum, parse, diff --git a/app/src/data/entities/EntityManager.ts b/app/src/data/entities/EntityManager.ts index 6757170..654c77d 100644 --- a/app/src/data/entities/EntityManager.ts +++ b/app/src/data/entities/EntityManager.ts @@ -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"; diff --git a/app/src/data/entities/mutation/MutatorResult.ts b/app/src/data/entities/mutation/MutatorResult.ts index 05da017..a0fe307 100644 --- a/app/src/data/entities/mutation/MutatorResult.ts +++ b/app/src/data/entities/mutation/MutatorResult.ts @@ -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 extends Result { onError: (error) => { if (!options?.silent) { $console.error("[ERROR] Mutator:", error.message); + throw error; } }, ...options, diff --git a/app/src/data/entities/query/Repository.ts b/app/src/data/entities/query/Repository.ts index f41bd8b..9fdbe99 100644 --- a/app/src/data/entities/query/Repository.ts +++ b/app/src/data/entities/query/Repository.ts @@ -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): RepoQuery { const entity = this.entity; // @todo: if not cloned deep, it will keep references and error if multiple requests come in const validated = { diff --git a/app/src/data/entities/query/RepositoryResult.ts b/app/src/data/entities/query/RepositoryResult.ts index fee0dff..7631f8f 100644 --- a/app/src/data/entities/query/RepositoryResult.ts +++ b/app/src/data/entities/query/RepositoryResult.ts @@ -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; diff --git a/app/src/data/events/index.ts b/app/src/data/events/index.ts index b10f3b6..246a39d 100644 --- a/app/src/data/events/index.ts +++ b/app/src/data/events/index.ts @@ -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"; diff --git a/app/src/data/fields/DateField.ts b/app/src/data/fields/DateField.ts index 0fdf91e..504273c 100644 --- a/app/src/data/fields/DateField.ts +++ b/app/src/data/fields/DateField.ts @@ -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; diff --git a/app/src/data/index.ts b/app/src/data/index.ts index 9fb5a35..32e914d 100644 --- a/app/src/data/index.ts +++ b/app/src/data/index.ts @@ -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, diff --git a/app/src/data/schema/SchemaManager.ts b/app/src/data/schema/SchemaManager.ts index ab5fbda..dafd616 100644 --- a/app/src/data/schema/SchemaManager.ts +++ b/app/src/data/schema/SchemaManager.ts @@ -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[]; diff --git a/app/src/data/server/query.ts b/app/src/data/server/query.ts index b4ebc9e..0512296 100644 --- a/app/src/data/server/query.ts +++ b/app/src/data/server/query.ts @@ -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; +export type RepoQuery = s.StaticCoerced & { + sort: SortSchema; +}; diff --git a/app/src/flows/flows/Execution.ts b/app/src/flows/flows/Execution.ts index 61bf05e..41d2166 100644 --- a/app/src/flows/flows/Execution.ts +++ b/app/src/flows/flows/Execution.ts @@ -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; diff --git a/app/src/flows/flows/Flow.ts b/app/src/flows/flows/Flow.ts index c924756..cf6a00b 100644 --- a/app/src/flows/flows/Flow.ts +++ b/app/src/flows/flows/Flow.ts @@ -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 object }> = ReturnType; diff --git a/app/src/flows/flows/executors/RuntimeExecutor.ts b/app/src/flows/flows/executors/RuntimeExecutor.ts index 46ea105..55bf890 100644 --- a/app/src/flows/flows/executors/RuntimeExecutor.ts +++ b/app/src/flows/flows/executors/RuntimeExecutor.ts @@ -1,5 +1,5 @@ import type { Task } from "../../tasks/Task"; -import { $console } from "core"; +import { $console } from "core/utils"; export class RuntimeExecutor { async run( diff --git a/app/src/flows/flows/triggers/EventTrigger.ts b/app/src/flows/flows/triggers/EventTrigger.ts index 3924840..e79b3a0 100644 --- a/app/src/flows/flows/triggers/EventTrigger.ts +++ b/app/src/flows/flows/triggers/EventTrigger.ts @@ -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; diff --git a/app/src/flows/tasks/presets/LogTask.ts b/app/src/flows/tasks/presets/LogTask.ts index fda6d12..8023daf 100644 --- a/app/src/flows/tasks/presets/LogTask.ts +++ b/app/src/flows/tasks/presets/LogTask.ts @@ -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; diff --git a/app/src/index.ts b/app/src/index.ts index f9c1e63..afac83d 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -16,6 +16,7 @@ export { type ModuleManagerOptions, type ModuleBuildContext, type InitialModuleConfigs, + ModuleManagerEvents, } from "./modules/ModuleManager"; export type { ServerEnv } from "modules/Controller"; diff --git a/app/src/media/AppMedia.ts b/app/src/media/AppMedia.ts index 18b536e..36fce41 100644 --- a/app/src/media/AppMedia.ts +++ b/app/src/media/AppMedia.ts @@ -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"; diff --git a/app/src/media/storage/Storage.ts b/app/src/media/storage/Storage.ts index ae66070..f8e73cb 100644 --- a/app/src/media/storage/Storage.ts +++ b/app/src/media/storage/Storage.ts @@ -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 = { diff --git a/app/src/modules/ModuleApi.ts b/app/src/modules/ModuleApi.ts index ebb6403..f8a295c 100644 --- a/app/src/modules/ModuleApi.ts +++ b/app/src/modules/ModuleApi.ts @@ -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"; diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index 825a220..c79aeb6 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -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 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; @@ -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; @@ -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(); }); diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index aa109ee..35ebc81 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -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"; diff --git a/app/src/modules/server/AppServer.ts b/app/src/modules/server/AppServer.ts index ba1c566..2c4cb76 100644 --- a/app/src/modules/server/AppServer.ts +++ b/app/src/modules/server/AppServer.ts @@ -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"; diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index ec07831..9e65315 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -1,9 +1,8 @@ /// import type { App } from "App"; -import { $console, tbValidator as tb } from "core"; import { - StringEnum, + $console, TypeInvalidError, datetimeStringLocal, datetimeStringUTC, diff --git a/app/vite.dev.ts b/app/vite.dev.ts index 9181dc9..1274d21 100644 --- a/app/vite.dev.ts +++ b/app/vite.dev.ts @@ -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); diff --git a/bun.lock b/bun.lock index 7a37352..434b81f 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="],