various fixes: refactored imports, introduced fromDriver/toDriver to improve compat

This commit is contained in:
dswbx
2025-06-13 21:15:29 +02:00
parent cc038a0a9a
commit 2ada4e9f20
15 changed files with 100 additions and 35 deletions

View File

@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.14.0", "version": "0.15.0-rc.0",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {

View File

@@ -69,8 +69,9 @@ export async function createAdapterApp<Config extends BkndConfig = BkndConfig, A
connection = config.connection; connection = config.connection;
} else { } else {
const sqlite = (await import("bknd/adapter/sqlite")).sqlite; const sqlite = (await import("bknd/adapter/sqlite")).sqlite;
connection = sqlite(config.connection ?? { url: ":memory:" }); const conf = config.connection ?? { url: ":memory:" };
$console.info(`Using ${connection.name} connection`); connection = sqlite(conf);
$console.info(`Using ${connection.name} connection`, conf.url);
} }
appConfig.connection = connection; appConfig.connection = connection;
} }

View File

@@ -73,6 +73,7 @@ type MakeAppConfig = {
async function makeApp(config: MakeAppConfig) { async function makeApp(config: MakeAppConfig) {
return await createRuntimeApp({ return await createRuntimeApp({
serveStatic: await serveStatic(config.server?.platform ?? "node"), serveStatic: await serveStatic(config.server?.platform ?? "node"),
...config,
}); });
} }
@@ -99,7 +100,7 @@ export async function makeAppFromEnv(options: Partial<RunOptions> = {}) {
let app: App | undefined = undefined; let app: App | undefined = undefined;
// first start from arguments if given // first start from arguments if given
if (options.dbUrl) { if (options.dbUrl) {
console.info("Using connection from", c.cyan("--db-url")); console.info("Using connection from", c.cyan("--db-url"), c.cyan(options.dbUrl));
const connection = options.dbUrl ? { url: options.dbUrl } : undefined; const connection = options.dbUrl ? { url: options.dbUrl } : undefined;
app = await makeApp({ connection, server: { platform: options.server } }); app = await makeApp({ connection, server: { platform: options.server } });

View File

@@ -20,6 +20,7 @@ import {
import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector"; import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector";
import type { Constructor, DB } from "core"; import type { Constructor, DB } from "core";
import { KyselyPluginRunner } from "data/plugins/KyselyPluginRunner"; import { KyselyPluginRunner } from "data/plugins/KyselyPluginRunner";
import type { Field } from "data/fields/Field";
export type QB = SelectQueryBuilder<any, any, any>; export type QB = SelectQueryBuilder<any, any, any>;
@@ -200,6 +201,14 @@ export abstract class Connection<Client = unknown> {
abstract getFieldSchema(spec: FieldSpec, strict?: boolean): SchemaResponse; abstract getFieldSchema(spec: FieldSpec, strict?: boolean): SchemaResponse;
toDriver(value: unknown, field: Field): unknown {
return value;
}
fromDriver(value: any, field: Field): unknown {
return value;
}
async close(): Promise<void> { async close(): Promise<void> {
// no-op by default // no-op by default
} }

View File

@@ -1,6 +1,8 @@
import type { TestRunner } from "core/test"; import type { TestRunner } from "core/test";
import { Connection, type FieldSpec } from "./Connection"; import { Connection, type FieldSpec } from "./Connection";
// @todo: add various datatypes: string, number, boolean, object, array, null, undefined, date, etc.
export function connectionTestSuite( export function connectionTestSuite(
testRunner: TestRunner, testRunner: TestRunner,
{ {

View File

@@ -57,6 +57,6 @@ export class LibsqlConnection extends SqliteConnection<Client> {
} }
} }
export function libsql(credentials: LibSqlCredentials): LibsqlConnection { export function libsql(credentials: Client | LibSqlCredentials): LibsqlConnection {
return new LibsqlConnection(credentials); return new LibsqlConnection(credentials);
} }

View File

@@ -11,7 +11,9 @@ import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } fro
import type { Constructor } from "core"; import type { Constructor } from "core";
import { customIntrospector } from "../Connection"; import { customIntrospector } from "../Connection";
import { SqliteIntrospector } from "./SqliteIntrospector"; import { SqliteIntrospector } from "./SqliteIntrospector";
import type { Field } from "data/fields/Field";
// @todo: add pragmas
export type SqliteConnectionConfig< export type SqliteConnectionConfig<
CustomDialect extends Constructor<Dialect> = Constructor<Dialect>, CustomDialect extends Constructor<Dialect> = Constructor<Dialect>,
> = { > = {
@@ -80,4 +82,24 @@ export abstract class SqliteConnection<Client = unknown> extends Connection<Clie
}, },
] as const; ] as const;
} }
override toDriver(value: unknown, field: Field): unknown {
if (field.type === "boolean") {
return value ? 1 : 0;
}
if (typeof value === "undefined") {
return null;
}
return value;
}
override fromDriver(value: any, field: Field): unknown {
if (field.type === "boolean" && typeof value === "number") {
return value === 1;
}
if (value === null) {
return undefined;
}
return value;
}
} }

View File

@@ -1,13 +1,9 @@
import { type Static, StringEnum, StringRecord, objectTransform } from "core/utils"; import { type Static, StringEnum, StringRecord, objectTransform } from "core/utils";
import * as tb from "@sinclair/typebox"; import * as tb from "@sinclair/typebox";
import {
FieldClassMap,
RelationClassMap,
RelationFieldClassMap,
entityConfigSchema,
entityTypes,
} from "data";
import { MediaField, mediaFieldConfigSchema } from "../media/MediaField"; import { MediaField, mediaFieldConfigSchema } from "../media/MediaField";
import { FieldClassMap } from "data/fields";
import { RelationClassMap, RelationFieldClassMap } from "data/relations";
import { entityConfigSchema, entityTypes } from "data/entities";
import { primaryFieldTypes } from "./fields"; import { primaryFieldTypes } from "./fields";
export const FIELDS = { export const FIELDS = {

View File

@@ -278,6 +278,10 @@ export class EntityManager<TBD extends object = DefaultDB> {
row[key] = field.getDefault(); row[key] = field.getDefault();
} }
// transform from driver
value = this.connection.fromDriver(value, field);
// transform from field
row[key] = field.transformRetrieve(value as any); row[key] = field.transformRetrieve(value as any);
} catch (e: any) { } catch (e: any) {
throw new TransformRetrieveFailedException( throw new TransformRetrieveFailedException(

View File

@@ -1,13 +1,15 @@
import { $console, type DB as DefaultDB, type PrimaryFieldType } from "core"; import type { DB as DefaultDB, PrimaryFieldType } from "core";
import { type EmitsEvents, EventManager } from "core/events"; import { type EmitsEvents, EventManager } from "core/events";
import type { DeleteQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder } from "kysely"; import type { DeleteQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder } from "kysely";
import { type TActionContext, WhereBuilder } from "../.."; import type { TActionContext } from "../..";
import { WhereBuilder } from "../query/WhereBuilder";
import type { Entity, EntityData, EntityManager } from "../../entities"; import type { Entity, EntityData, EntityManager } from "../../entities";
import { InvalidSearchParamsException } from "../../errors"; import { InvalidSearchParamsException } from "../../errors";
import { MutatorEvents } from "../../events"; import { MutatorEvents } from "../../events";
import { RelationMutator } from "../../relations"; import { RelationMutator } from "../../relations";
import type { RepoQuery } from "../../server/query"; import type { RepoQuery } from "../../server/query";
import { MutatorResult, type MutatorResultOptions } from "./MutatorResult"; import { MutatorResult, type MutatorResultOptions } from "./MutatorResult";
import { transformObject } from "core/utils";
type MutatorQB = type MutatorQB =
| InsertQueryBuilder<any, any, any> | InsertQueryBuilder<any, any, any>
@@ -86,7 +88,11 @@ export class Mutator<
throw new Error(`Field "${key}" is not fillable on entity "${entity.name}"`); throw new Error(`Field "${key}" is not fillable on entity "${entity.name}"`);
} }
// transform from field
validatedData[key] = await field.transformPersist(data[key], this.em, context); validatedData[key] = await field.transformPersist(data[key], this.em, context);
// transform to driver
validatedData[key] = this.em.connection.toDriver(validatedData[key], field);
} }
if (Object.keys(validatedData).length === 0) { if (Object.keys(validatedData).length === 0) {
@@ -283,6 +289,10 @@ export class Mutator<
): Promise<MutatorResult<Output[]>> { ): Promise<MutatorResult<Output[]>> {
const entity = this.entity; const entity = this.entity;
const validatedData = await this.getValidatedData(data, "update"); const validatedData = await this.getValidatedData(data, "update");
console.log("updateWhere", {
entity,
validatedData,
});
// @todo: add a way to delete all by adding force? // @todo: add a way to delete all by adding force?
if (!where || typeof where !== "object" || Object.keys(where).length === 0) { if (!where || typeof where !== "object" || Object.keys(where).length === 0) {

View File

@@ -5,6 +5,7 @@ import { Result, type ResultJSON, type ResultOptions } from "../Result";
export type MutatorResultOptions = ResultOptions & { export type MutatorResultOptions = ResultOptions & {
silent?: boolean; silent?: boolean;
logParams?: boolean;
}; };
export type MutatorResultJSON<T = EntityData[]> = ResultJSON<T>; export type MutatorResultJSON<T = EntityData[]> = ResultJSON<T>;
@@ -19,7 +20,10 @@ export class MutatorResult<T = EntityData[]> extends Result<T> {
hydrator: (rows) => em.hydrate(entity.name, rows as any), hydrator: (rows) => em.hydrate(entity.name, rows as any),
beforeExecute: (compiled) => { beforeExecute: (compiled) => {
if (!options?.silent) { if (!options?.silent) {
$console.debug(`[Mutation]\n${compiled.sql}\n`); $console.debug(
`[Mutation]\n${compiled.sql}\n`,
options?.logParams ? compiled.parameters : undefined,
);
} }
}, },
onError: (error) => { onError: (error) => {

View File

@@ -246,8 +246,10 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
qb = WhereBuilder.addClause(qb, options.where); qb = WhereBuilder.addClause(qb, options.where);
} }
if (!ignore.includes("limit")) qb = qb.limit(options.limit ?? defaults.limit); if (!ignore.includes("limit")) {
qb = qb.limit(options.limit ?? defaults.limit);
if (!ignore.includes("offset")) qb = qb.offset(options.offset ?? defaults.offset); if (!ignore.includes("offset")) qb = qb.offset(options.offset ?? defaults.offset);
}
// sorting // sorting
if (!ignore.includes("sort")) { if (!ignore.includes("sort")) {

View File

@@ -1,5 +1,5 @@
import { s } from "core/object/schema"; import { s } from "core/object/schema";
import { WhereBuilder, type WhereQuery } from "data"; import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
import { $console } from "core"; import { $console } from "core";
import { isObject } from "core/utils"; import { isObject } from "core/utils";
import type { CoercionOptions, TAnyOf } from "jsonv-ts"; import type { CoercionOptions, TAnyOf } from "jsonv-ts";

View File

@@ -1,5 +1,5 @@
import type { Static } from "core/utils"; import type { Static } from "core/utils";
import { Field, baseFieldConfigSchema } from "data"; import { Field, baseFieldConfigSchema } from "data/fields";
import * as tbbox from "@sinclair/typebox"; import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox; const { Type } = tbbox;

View File

@@ -8,25 +8,39 @@ import { __bknd } from "modules/ModuleManager";
import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection"; import { nodeSqlite } from "./src/adapter/node/connection/NodeSqliteConnection";
import { libsql } from "./src/data/connection/sqlite/LibsqlConnection"; import { libsql } from "./src/data/connection/sqlite/LibsqlConnection";
import { $console } from "core"; import { $console } from "core";
import { createClient } from "@libsql/client";
registries.media.register("local", StorageLocalAdapter); registries.media.register("local", StorageLocalAdapter);
const example = import.meta.env.VITE_EXAMPLE; const example = import.meta.env.VITE_EXAMPLE;
const dbUrl = example ? `file:.configs/${example}.db` : import.meta.env.VITE_DB_URL;
let connection: Connection; let connection: Connection;
if (dbUrl) {
connection = nodeSqlite({ url: dbUrl }); if (import.meta.env.VITE_DB_LIBSQL_URL) {
$console.debug("Using node-sqlite connection", dbUrl); connection = libsql(
} else if (import.meta.env.VITE_DB_LIBSQL_URL) { createClient({
connection = libsql({
url: import.meta.env.VITE_DB_LIBSQL_URL!, url: import.meta.env.VITE_DB_LIBSQL_URL!,
authToken: import.meta.env.VITE_DB_LIBSQL_TOKEN!, authToken: import.meta.env.VITE_DB_LIBSQL_TOKEN!,
}); }),
);
$console.debug("Using libsql connection", import.meta.env.VITE_DB_URL); $console.debug("Using libsql connection", import.meta.env.VITE_DB_URL);
} else { } else {
const dbUrl = example ? `file:.configs/${example}.db` : import.meta.env.VITE_DB_URL;
if (dbUrl) {
connection = nodeSqlite({ url: dbUrl });
$console.debug("Using node-sqlite connection", dbUrl);
} else if (import.meta.env.VITE_DB_LIBSQL_URL) {
connection = libsql(
createClient({
url: import.meta.env.VITE_DB_LIBSQL_URL!,
authToken: import.meta.env.VITE_DB_LIBSQL_TOKEN!,
}),
);
$console.debug("Using libsql connection", import.meta.env.VITE_DB_URL);
} else {
connection = nodeSqlite(); connection = nodeSqlite();
$console.debug("No connection provided, using in-memory database"); $console.debug("No connection provided, using in-memory database");
}
} }
/* if (example) { /* if (example) {