move postgres to a separate package

This commit is contained in:
dswbx
2025-03-14 09:42:51 +01:00
parent b1b2e97a5a
commit c4aaa5a90b
20 changed files with 381 additions and 80 deletions

View File

@@ -53,6 +53,9 @@ function banner(title: string) {
console.log("-".repeat(40)); console.log("-".repeat(40));
} }
// collection of always-external packages
const external = ["bun:test", "@libsql/client"] as const;
/** /**
* Building backend and general API * Building backend and general API
*/ */
@@ -64,7 +67,7 @@ async function buildApi() {
watch, watch,
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"], entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
outDir: "dist", outDir: "dist",
external: ["bun:test", "@libsql/client"], external: [...external],
metafile: true, metafile: true,
platform: "browser", platform: "browser",
format: ["esm"], format: ["esm"],
@@ -93,7 +96,7 @@ async function buildUi() {
sourcemap, sourcemap,
watch, watch,
external: [ external: [
"bun:test", ...external,
"react", "react",
"react-dom", "react-dom",
"react/jsx-runtime", "react/jsx-runtime",

View File

@@ -85,7 +85,6 @@
"kysely-d1": "^0.3.0", "kysely-d1": "^0.3.0",
"open": "^10.1.0", "open": "^10.1.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"pg": "^8.13.3",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",

View File

@@ -18,6 +18,10 @@ class CustomD1Dialect extends D1Dialect {
} }
export class D1Connection extends SqliteConnection { export class D1Connection extends SqliteConnection {
protected override readonly supported = {
batching: true,
};
constructor(private config: D1ConnectionConfig) { constructor(private config: D1ConnectionConfig) {
const plugins = [new ParseJSONResultsPlugin()]; const plugins = [new ParseJSONResultsPlugin()];
@@ -28,14 +32,6 @@ export class D1Connection extends SqliteConnection {
super(kysely, {}, plugins); super(kysely, {}, plugins);
} }
override supportsBatching(): boolean {
return true;
}
override supportsIndices(): boolean {
return true;
}
protected override async batch<Queries extends QB[]>( protected override async batch<Queries extends QB[]>(
queries: [...Queries], queries: [...Queries],
): Promise<{ ): Promise<{

View File

@@ -2,7 +2,6 @@ import {
type AliasableExpression, type AliasableExpression,
type ColumnBuilderCallback, type ColumnBuilderCallback,
type ColumnDataType, type ColumnDataType,
type DatabaseIntrospector,
type Expression, type Expression,
type Kysely, type Kysely,
type KyselyPlugin, type KyselyPlugin,
@@ -77,6 +76,9 @@ const CONN_SYMBOL = Symbol.for("bknd:connection");
export abstract class Connection<DB = any> { export abstract class Connection<DB = any> {
kysely: Kysely<DB>; kysely: Kysely<DB>;
protected readonly supported = {
batching: false,
};
constructor( constructor(
kysely: Kysely<DB>, kysely: Kysely<DB>,
@@ -101,13 +103,8 @@ export abstract class Connection<DB = any> {
return this.kysely.introspection as any; return this.kysely.introspection as any;
} }
supportsBatching(): boolean { supports(feature: keyof typeof this.supported): boolean {
return false; return this.supported[feature] ?? false;
}
// @todo: add if only first field is used in index
supportsIndices(): boolean {
return false;
} }
async ping(): Promise<boolean> { async ping(): Promise<boolean> {
@@ -129,7 +126,7 @@ export abstract class Connection<DB = any> {
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>; [K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
}> { }> {
// bypass if no client support // bypass if no client support
if (!this.supportsBatching()) { if (!this.supports("batching")) {
const data: any = []; const data: any = [];
for (const q of queries) { for (const q of queries) {
const result = await q.execute(); const result = await q.execute();
@@ -151,5 +148,8 @@ export abstract class Connection<DB = any> {
} }
abstract getFieldSchema(spec: FieldSpec, strict?: boolean): SchemaResponse; abstract getFieldSchema(spec: FieldSpec, strict?: boolean): SchemaResponse;
abstract close(): Promise<void>;
async close(): Promise<void> {
// no-op by default
}
} }

View File

@@ -1,6 +1,10 @@
import { Connection, type FieldSpec, type SchemaResponse } from "./Connection"; import { Connection, type FieldSpec, type SchemaResponse } from "./Connection";
export class DummyConnection extends Connection { export class DummyConnection extends Connection {
protected override readonly supported = {
batching: true,
};
constructor() { constructor() {
super(undefined as any); super(undefined as any);
} }

View File

@@ -1,12 +1,14 @@
export { Connection } from "./Connection";
export { BaseIntrospector } from "./BaseIntrospector"; export { BaseIntrospector } from "./BaseIntrospector";
export {
Connection,
type FieldSpec,
type IndexSpec,
type DbFunctions,
type SchemaResponse,
} from "./Connection";
// sqlite // sqlite
export { LibsqlConnection, type LibSqlCredentials } from "./sqlite/LibsqlConnection"; export { LibsqlConnection, type LibSqlCredentials } from "./sqlite/LibsqlConnection";
export { SqliteConnection } from "./sqlite/SqliteConnection"; export { SqliteConnection } from "./sqlite/SqliteConnection";
export { SqliteLocalConnection } from "./sqlite/SqliteLocalConnection";
export { SqliteIntrospector } from "./sqlite/SqliteIntrospector"; export { SqliteIntrospector } from "./sqlite/SqliteIntrospector";
export { SqliteLocalConnection } from "./sqlite/SqliteLocalConnection";
// postgres
export { PostgresConnection, type PostgresConnectionConfig } from "./postgres/PostgresConnection";
export { PostgresIntrospector } from "./postgres/PostgresIntrospector";

View File

@@ -1,8 +1,8 @@
import { type Client, type Config, type InStatement, createClient } from "@libsql/client"; import { type Client, type Config, type InStatement, createClient } from "@libsql/client";
import { LibsqlDialect } from "@libsql/kysely-libsql"; import { LibsqlDialect } from "@libsql/kysely-libsql";
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
import { FilterNumericKeysPlugin } from "data/plugins/FilterNumericKeysPlugin"; import { FilterNumericKeysPlugin } from "data/plugins/FilterNumericKeysPlugin";
import { KyselyPluginRunner } from "data/plugins/KyselyPluginRunner"; import { KyselyPluginRunner } from "data/plugins/KyselyPluginRunner";
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
import type { QB } from "../Connection"; import type { QB } from "../Connection";
import { SqliteConnection } from "./SqliteConnection"; import { SqliteConnection } from "./SqliteConnection";
import { SqliteIntrospector } from "./SqliteIntrospector"; import { SqliteIntrospector } from "./SqliteIntrospector";
@@ -25,6 +25,9 @@ class CustomLibsqlDialect extends LibsqlDialect {
export class LibsqlConnection extends SqliteConnection { export class LibsqlConnection extends SqliteConnection {
private client: Client; private client: Client;
protected override readonly supported = {
batching: true,
};
constructor(client: Client); constructor(client: Client);
constructor(credentials: LibSqlCredentials); constructor(credentials: LibSqlCredentials);
@@ -53,14 +56,6 @@ export class LibsqlConnection extends SqliteConnection {
this.client = client; this.client = client;
} }
override supportsBatching(): boolean {
return true;
}
override supportsIndices(): boolean {
return true;
}
getClient(): Client { getClient(): Client {
return this.client; return this.client;
} }

View File

@@ -16,10 +16,6 @@ export class SqliteConnection extends Connection {
); );
} }
override supportsIndices(): boolean {
return true;
}
override getFieldSchema(spec: FieldSpec): SchemaResponse { override getFieldSchema(spec: FieldSpec): SchemaResponse {
this.validateFieldSpecType(spec.type); this.validateFieldSpecType(spec.type);
let type: ColumnDataType = spec.type; let type: ColumnDataType = spec.type;
@@ -47,8 +43,4 @@ export class SqliteConnection extends Connection {
}, },
] as const; ] as const;
} }
override async close(): Promise<void> {
// no-op
}
} }

View File

@@ -1,5 +1,10 @@
import { type DatabaseIntrospector, ParseJSONResultsPlugin, type SqliteDatabase } from "kysely"; import {
import { Kysely, SqliteDialect } from "kysely"; type DatabaseIntrospector,
Kysely,
ParseJSONResultsPlugin,
type SqliteDatabase,
SqliteDialect,
} from "kysely";
import { SqliteConnection } from "./SqliteConnection"; import { SqliteConnection } from "./SqliteConnection";
import { SqliteIntrospector } from "./SqliteIntrospector"; import { SqliteIntrospector } from "./SqliteIntrospector";
@@ -23,8 +28,4 @@ export class SqliteLocalConnection extends SqliteConnection {
super(kysely, {}, plugins); super(kysely, {}, plugins);
} }
override supportsIndices(): boolean {
return true;
}
} }

View File

@@ -49,10 +49,6 @@ export class SchemaManager {
constructor(private readonly em: EntityManager<any>) {} constructor(private readonly em: EntityManager<any>) {}
private getIntrospector() { private getIntrospector() {
if (!this.em.connection.supportsIndices()) {
throw new Error("Indices are not supported by the current connection");
}
return this.em.connection.getIntrospector(); return this.em.connection.getIntrospector();
} }

View File

@@ -83,7 +83,6 @@
"kysely-d1": "^0.3.0", "kysely-d1": "^0.3.0",
"open": "^10.1.0", "open": "^10.1.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"pg": "^8.13.3",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
@@ -139,6 +138,22 @@
"react-dom": ">=18", "react-dom": ">=18",
}, },
}, },
"packages/postgres": {
"name": "@bknd/postgres",
"version": "0.0.1",
"dependencies": {
"kysely": "^0.27.6",
"pg": "^8.12.0",
},
"devDependencies": {
"@types/bun": "^1.2.5",
"@types/node": "^22.13.10",
"@types/pg": "^8.11.11",
"bknd": "workspace:*",
"tsup": "^8.4.0",
"typescript": "^5.6.3",
},
},
}, },
"packages": { "packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -447,6 +462,8 @@
"@bknd/plasmic": ["@bknd/plasmic@workspace:packages/plasmic"], "@bknd/plasmic": ["@bknd/plasmic@workspace:packages/plasmic"],
"@bknd/postgres": ["@bknd/postgres@workspace:packages/postgres"],
"@bluwy/giget-core": ["@bluwy/giget-core@0.1.2", "", { "dependencies": { "tar": "^6.2.1" } }, "sha512-v9f+ueUOKkZCDKiCm0yxKtYgYNLD9zlKarNux0NSXOvNm94QEYL3RlMpGKgD2hq44pbF2qWqEmHnCvmk56kPJw=="], "@bluwy/giget-core": ["@bluwy/giget-core@0.1.2", "", { "dependencies": { "tar": "^6.2.1" } }, "sha512-v9f+ueUOKkZCDKiCm0yxKtYgYNLD9zlKarNux0NSXOvNm94QEYL3RlMpGKgD2hq44pbF2qWqEmHnCvmk56kPJw=="],
"@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="],
@@ -1171,6 +1188,8 @@
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
"@types/pg": ["@types/pg@8.11.11", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw=="],
"@types/prettier": ["@types/prettier@1.19.1", "", {}, "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ=="], "@types/prettier": ["@types/prettier@1.19.1", "", {}, "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ=="],
"@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="], "@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="],
@@ -2527,6 +2546,8 @@
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
"obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="],
"ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], "ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="],
"on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], "on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="],
@@ -2605,11 +2626,13 @@
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="],
"pg-pool": ["pg-pool@3.8.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw=="], "pg-pool": ["pg-pool@3.8.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw=="],
"pg-protocol": ["pg-protocol@1.8.0", "", {}, "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g=="], "pg-protocol": ["pg-protocol@1.8.0", "", {}, "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], "pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
@@ -2661,13 +2684,15 @@
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="], "postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], "postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], "postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
"prelude-ls": ["prelude-ls@1.1.2", "", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="], "prelude-ls": ["prelude-ls@1.1.2", "", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="],
@@ -3981,6 +4006,8 @@
"peek-stream/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="], "peek-stream/duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
"pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
@@ -4351,6 +4378,14 @@
"ora/log-symbols/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], "ora/log-symbols/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
"pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"progress-estimator/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "progress-estimator/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"progress-estimator/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "progress-estimator/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],

View File

@@ -0,0 +1,50 @@
# Postgres adapter for `bknd` (experimental)
This packages adds an adapter to use a Postgres database with `bknd`. It is based on `pg` and the driver included in `kysely`.
## Installation
Install the adapter with:
```bash
npm install @bknd/postgres
```
## Usage
Create a connection:
```ts
import { PostgresConnection } from "@bknd/postgres";
const connection = new PostgresConnection({
host: "localhost",
port: 5432,
user: "postgres",
password: "postgres",
database: "bknd",
});
```
Use the connection depending on which framework or runtime you are using. E.g., when using `createApp`, you can use the connection as follows:
```ts
import { createApp } from "bknd";
import { PostgresConnection } from "@bknd/postgres";
const connection = new PostgresConnection();
const app = createApp({ connection });
```
Or if you're using it with a framework, say Next.js, you can add the connection object to where you're initializating the app:
```ts
// e.g. in src/app/api/[[...bknd]]/route.ts
import { serve } from "bknd/adapter/nextjs";
import { PostgresConnection } from "@bknd/postgres";
const connection = new PostgresConnection();
const handler = serve({
connection
})
// ...
```
For more information about how to integrate Next.js in general, check out the [Next.js documentation](https://docs.bknd.io/integration/nextjs).

View File

@@ -0,0 +1,37 @@
{
"name": "@bknd/postgres",
"version": "0.0.1",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup",
"test": "bun test",
"docker:start": "docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_DB=bknd -p 5430:5432 postgres:17",
"docker:stop": "docker stop bknd-test-postgres"
},
"dependencies": {
"pg": "^8.12.0",
"kysely": "^0.27.6"
},
"devDependencies": {
"@types/bun": "^1.2.5",
"@types/node": "^22.13.10",
"@types/pg": "^8.11.11",
"bknd": "workspace:*",
"tsup": "^8.4.0",
"typescript": "^5.6.3"
},
"tsup": {
"entry": ["src/index.ts"],
"format": ["esm"],
"target": "es2022",
"clean": true,
"minify": true,
"dts": true,
"metafile": true,
"external": ["bknd", "pg", "kysely"]
},
"files": ["dist", "!*.map", "!metafile*.json"]
}

View File

@@ -1,31 +1,34 @@
import { Connection, type FieldSpec, type SchemaResponse } from "bknd/data";
import { import {
Kysely,
PostgresDialect,
type DatabaseIntrospector,
type ColumnDataType, type ColumnDataType,
type ColumnDefinitionBuilder, type ColumnDefinitionBuilder,
type DatabaseIntrospector,
Kysely,
ParseJSONResultsPlugin, ParseJSONResultsPlugin,
PostgresDialect,
type SelectQueryBuilder,
} from "kysely"; } from "kysely";
import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/postgres";
import pg from "pg"; import pg from "pg";
import { PostgresIntrospector } from "./PostgresIntrospector"; import { PostgresIntrospector } from "./PostgresIntrospector";
import {
type FieldSpec,
type SchemaResponse,
Connection,
type QB,
} from "data/connection/Connection";
export type PostgresConnectionConfig = pg.PoolConfig; export type PostgresConnectionConfig = pg.PoolConfig;
export type QB = SelectQueryBuilder<any, any, any>;
const plugins = [new ParseJSONResultsPlugin()]; const plugins = [new ParseJSONResultsPlugin()];
class CustomPostgresDialect extends PostgresDialect { class CustomPostgresDialect extends PostgresDialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector { override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new PostgresIntrospector(db); return new PostgresIntrospector(db, {
excludeTables: [],
});
} }
} }
export class PostgresConnection extends Connection { export class PostgresConnection extends Connection {
protected override readonly supported = {
batching: true,
};
private pool: pg.Pool; private pool: pg.Pool;
constructor(config: PostgresConnectionConfig) { constructor(config: PostgresConnectionConfig) {
@@ -38,14 +41,18 @@ export class PostgresConnection extends Connection {
//log: ["query", "error"], //log: ["query", "error"],
}); });
super(kysely, {}, plugins); super(
kysely,
{
jsonArrayFrom,
jsonBuildObject,
jsonObjectFrom,
},
plugins,
);
this.pool = pool; this.pool = pool;
} }
override supportsIndices(): boolean {
return true;
}
override getFieldSchema(spec: FieldSpec): SchemaResponse { override getFieldSchema(spec: FieldSpec): SchemaResponse {
this.validateFieldSpecType(spec.type); this.validateFieldSpecType(spec.type);
let type: ColumnDataType = spec.primary ? "serial" : spec.type; let type: ColumnDataType = spec.primary ? "serial" : spec.type;
@@ -83,10 +90,6 @@ export class PostgresConnection extends Connection {
]; ];
} }
override supportsBatching(): boolean {
return true;
}
override async close(): Promise<void> { override async close(): Promise<void> {
await this.pool.end(); await this.pool.end();
} }

View File

@@ -1,5 +1,5 @@
import { type SchemaMetadata, sql } from "kysely"; import { type SchemaMetadata, sql } from "kysely";
import { BaseIntrospector } from "data/connection/BaseIntrospector"; import { BaseIntrospector } from "bknd/data";
type PostgresSchemaSpec = { type PostgresSchemaSpec = {
name: string; name: string;

View File

@@ -0,0 +1,2 @@
export { PostgresConnection, type PostgresConnectionConfig } from "./PostgresConnection";
export { PostgresIntrospector } from "./PostgresIntrospector";

View File

@@ -0,0 +1,19 @@
import { describe, it, expect } from "bun:test";
import { PostgresConnection } from "../src";
import { createConnection, cleanDatabase } from "./setup";
describe(PostgresConnection, () => {
it("should connect to the database", async () => {
const connection = createConnection();
expect(await connection.ping()).toBe(true);
});
it("should clean the database", async () => {
const connection = createConnection();
await cleanDatabase(connection);
const tables = await connection.getIntrospector().getTables();
expect(tables).toEqual([]);
});
});

View File

@@ -0,0 +1,113 @@
import { describe, it, expect, beforeAll, afterAll, afterEach } from "bun:test";
import { createApp } from "bknd";
import * as proto from "bknd/data";
import { createConnection, cleanDatabase } from "./setup";
import type { PostgresConnection } from "../src";
let connection: PostgresConnection;
beforeAll(async () => {
connection = createConnection();
await cleanDatabase(connection);
});
afterEach(async () => {
await cleanDatabase(connection);
});
afterAll(async () => {
await connection.close();
});
describe("integration", () => {
it("should create app and ping", async () => {
const app = createApp({
connection,
});
await app.build();
expect(app.version()).toBeDefined();
expect(await app.em.ping()).toBe(true);
});
it("should create a basic schema", async () => {
const schema = 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,
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);
});
});

View File

@@ -0,0 +1,25 @@
import type { Kysely } from "kysely";
import { PostgresConnection, PostgresIntrospector, type PostgresConnectionConfig } from "../src";
export const info = {
host: "localhost",
port: 5430,
user: "postgres",
password: "postgres",
database: "bknd",
};
export function createConnection(config: PostgresConnectionConfig = {}) {
return new PostgresConnection({
...info,
...config,
});
}
export async function cleanDatabase(connection: PostgresConnection) {
const kysely = connection.kysely;
// drop all tables & create new schema
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
await kysely.schema.createSchema("public").execute();
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"composite": false,
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"target": "ES2022",
"noImplicitAny": false,
"allowJs": true,
"verbatimModuleSyntax": true,
"declaration": true,
"strict": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"exactOptionalPropertyTypes": false,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["./src/**/*.ts"],
"exclude": ["node_modules"]
}