Merge pull request #257 from bknd-io/fix/postgres-0.17-updates

postgres: bump 0.17.1 and improve custom connection API
This commit is contained in:
dswbx
2025-09-14 17:10:32 +02:00
committed by GitHub
12 changed files with 105 additions and 75 deletions

View File

@@ -258,6 +258,9 @@ export class EntityManager<TBD extends object = DefaultDB> {
// @todo: centralize and add tests // @todo: centralize and add tests
hydrate(entity_name: string, _data: EntityData[]) { hydrate(entity_name: string, _data: EntityData[]) {
if (!Array.isArray(_data) || _data.length === 0) {
return [];
}
const entity = this.entity(entity_name); const entity = this.entity(entity_name);
const data: EntityData[] = []; const data: EntityData[] = [];

View File

@@ -151,7 +151,6 @@
"bknd": "workspace:*", "bknd": "workspace:*",
"kysely-neon": "^1.3.0", "kysely-neon": "^1.3.0",
"tsup": "^8.4.0", "tsup": "^8.4.0",
"typescript": "^5.8.2",
}, },
"optionalDependencies": { "optionalDependencies": {
"kysely": "^0.27.6", "kysely": "^0.27.6",
@@ -3832,10 +3831,6 @@
"@bknd/plasmic/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "@bknd/plasmic/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"@bknd/postgres/@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
"@bknd/postgres/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"@bknd/sqlocal/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "@bknd/sqlocal/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"@bundled-es-modules/cookie/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "@bundled-es-modules/cookie/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
@@ -4684,8 +4679,6 @@
"@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="], "@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="],
"@bknd/postgres/@types/bun/bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], "@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], "@cloudflare/vitest-pool-workers/miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],

View File

@@ -224,7 +224,7 @@ Example using `@neondatabase/serverless`:
import { createCustomPostgresConnection } from "@bknd/postgres"; import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon"; import { NeonDialect } from "kysely-neon";
const neon = createCustomPostgresConnection(NeonDialect); const neon = createCustomPostgresConnection("neon", NeonDialect);
serve({ serve({
connection: neon({ connection: neon({
@@ -247,7 +247,7 @@ const xata = new client({
branch: process.env.XATA_BRANCH, branch: process.env.XATA_BRANCH,
}); });
const xataConnection = createCustomPostgresConnection(XataDialect, { const xataConnection = createCustomPostgresConnection("xata", XataDialect, {
supports: { supports: {
batching: false, batching: false,
}, },

View File

@@ -59,7 +59,7 @@ You can create a custom kysely postgres dialect by using the `createCustomPostgr
```ts ```ts
import { createCustomPostgresConnection } from "@bknd/postgres"; import { createCustomPostgresConnection } from "@bknd/postgres";
const connection = createCustomPostgresConnection(MyDialect)({ const connection = createCustomPostgresConnection("my_postgres_dialect", MyDialect)({
// your custom dialect configuration // your custom dialect configuration
supports: { supports: {
batching: true batching: true
@@ -75,7 +75,7 @@ const connection = createCustomPostgresConnection(MyDialect)({
import { createCustomPostgresConnection } from "@bknd/postgres"; import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon"; import { NeonDialect } from "kysely-neon";
const connection = createCustomPostgresConnection(NeonDialect)({ const connection = createCustomPostgresConnection("neon", NeonDialect)({
connectionString: process.env.NEON, connectionString: process.env.NEON,
}); });
``` ```
@@ -94,7 +94,7 @@ const xata = new client({
branch: process.env.XATA_BRANCH, branch: process.env.XATA_BRANCH,
}); });
const connection = createCustomPostgresConnection(XataDialect, { const connection = createCustomPostgresConnection("xata", XataDialect, {
supports: { supports: {
batching: false, batching: false,
}, },

View File

@@ -31,8 +31,7 @@
"@xata.io/kysely": "^0.2.1", "@xata.io/kysely": "^0.2.1",
"bknd": "workspace:*", "bknd": "workspace:*",
"kysely-neon": "^1.3.0", "kysely-neon": "^1.3.0",
"tsup": "^8.4.0", "tsup": "^8.4.0"
"typescript": "^5.8.2"
}, },
"tsup": { "tsup": {
"entry": ["src/index.ts"], "entry": ["src/index.ts"],

View File

@@ -1,12 +1,13 @@
import { Kysely, PostgresDialect } from "kysely"; import { Kysely, PostgresDialect } from "kysely";
import { PostgresIntrospector } from "./PostgresIntrospector"; import { PostgresIntrospector } from "./PostgresIntrospector";
import { PostgresConnection, plugins } from "./PostgresConnection"; import { PostgresConnection, plugins } from "./PostgresConnection";
import { customIntrospector } from "bknd/data"; import { customIntrospector } from "bknd";
import $pg from "pg"; import $pg from "pg";
export type PgPostgresConnectionConfig = $pg.PoolConfig; export type PgPostgresConnectionConfig = $pg.PoolConfig;
export class PgPostgresConnection extends PostgresConnection { export class PgPostgresConnection extends PostgresConnection {
override name = "pg";
private pool: $pg.Pool; private pool: $pg.Pool;
constructor(config: PgPostgresConnectionConfig) { constructor(config: PgPostgresConnectionConfig) {

View File

@@ -1,4 +1,11 @@
import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } from "bknd/data"; import {
Connection,
type DbFunctions,
type FieldSpec,
type SchemaResponse,
type ConnQuery,
type ConnQueryResults,
} from "bknd";
import { import {
ParseJSONResultsPlugin, ParseJSONResultsPlugin,
type ColumnDataType, type ColumnDataType,
@@ -13,12 +20,13 @@ export type QB = SelectQueryBuilder<any, any, any>;
export const plugins = [new ParseJSONResultsPlugin()]; export const plugins = [new ParseJSONResultsPlugin()];
export abstract class PostgresConnection<DB = any> extends Connection<DB> { export abstract class PostgresConnection extends Connection {
protected override readonly supported = { protected override readonly supported = {
batching: true, batching: true,
softscans: true,
}; };
constructor(kysely: Kysely<DB>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) { constructor(kysely: Kysely<any>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) {
super( super(
kysely, kysely,
fn ?? { fn ?? {
@@ -73,13 +81,9 @@ export abstract class PostgresConnection<DB = any> extends Connection<DB> {
]; ];
} }
protected override async batch<Queries extends QB[]>( override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
queries: [...Queries],
): Promise<{
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
}> {
return this.kysely.transaction().execute(async (trx) => { return this.kysely.transaction().execute(async (trx) => {
return Promise.all(queries.map((q) => trx.executeQuery(q).then((r) => r.rows))); return Promise.all(qbs.map((q) => trx.executeQuery(q)));
}) as any; }) as any;
} }
} }

View File

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

View File

@@ -1,13 +1,15 @@
import { Kysely } from "kysely"; import { Kysely } from "kysely";
import { PostgresIntrospector } from "./PostgresIntrospector"; import { PostgresIntrospector } from "./PostgresIntrospector";
import { PostgresConnection, plugins } from "./PostgresConnection"; import { PostgresConnection, plugins } from "./PostgresConnection";
import { customIntrospector } from "bknd/data"; import { customIntrospector } from "bknd";
import { PostgresJSDialect } from "kysely-postgres-js"; import { PostgresJSDialect } from "kysely-postgres-js";
import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres"; import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres";
export type PostgresJsConfig = Options<Record<string, PostgresType>>; export type PostgresJsConfig = Options<Record<string, PostgresType>>;
export class PostgresJsConnection extends PostgresConnection { export class PostgresJsConnection extends PostgresConnection {
override name = "postgres-js";
private postgres: Sql; private postgres: Sql;
constructor(opts: { postgres: Sql }) { constructor(opts: { postgres: Sql }) {

View File

@@ -1,9 +1,10 @@
import type { Constructor } from "bknd/core"; import { customIntrospector, type DbFunctions } from "bknd";
import { customIntrospector, type DbFunctions } from "bknd/data";
import { Kysely, type Dialect, type KyselyPlugin } from "kysely"; import { Kysely, type Dialect, type KyselyPlugin } from "kysely";
import { plugins, PostgresConnection } from "./PostgresConnection"; import { plugins, PostgresConnection } from "./PostgresConnection";
import { PostgresIntrospector } from "./PostgresIntrospector"; import { PostgresIntrospector } from "./PostgresIntrospector";
export type Constructor<T> = new (...args: any[]) => T;
export type CustomPostgresConnection = { export type CustomPostgresConnection = {
supports?: PostgresConnection["supported"]; supports?: PostgresConnection["supported"];
fn?: Partial<DbFunctions>; fn?: Partial<DbFunctions>;
@@ -15,17 +16,19 @@ export function createCustomPostgresConnection<
T extends Constructor<Dialect>, T extends Constructor<Dialect>,
C extends ConstructorParameters<T>[0], C extends ConstructorParameters<T>[0],
>( >(
name: string,
dialect: Constructor<Dialect>, dialect: Constructor<Dialect>,
options?: CustomPostgresConnection, options?: CustomPostgresConnection,
): (config: C) => PostgresConnection<any> { ): (config: C) => PostgresConnection {
const supported = { const supported = {
batching: true, batching: true,
...((options?.supports ?? {}) as any), ...((options?.supports ?? {}) as any),
}; };
return (config: C) => return (config: C) =>
new (class extends PostgresConnection<any> { new (class extends PostgresConnection {
protected override readonly supported = supported; override name = name;
override readonly supported = supported;
constructor(config: C) { constructor(config: C) {
super( super(

View File

@@ -1,8 +1,11 @@
import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test"; import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test";
import type { PostgresConnection } from "../src"; import type { PostgresConnection } from "../src";
import { createApp } from "bknd"; import { createApp, em, entity, text } from "bknd";
import * as proto from "bknd/data";
import { disableConsoleLog, enableConsoleLog } from "bknd/utils"; import { disableConsoleLog, enableConsoleLog } from "bknd/utils";
// @ts-ignore
import { connectionTestSuite } from "$bknd/data/connection/connection-test-suite";
// @ts-ignore
import { bunTestRunner } from "$bknd/adapter/bun/test";
export type TestSuiteConfig = { export type TestSuiteConfig = {
createConnection: () => InstanceType<typeof PostgresConnection>; createConnection: () => InstanceType<typeof PostgresConnection>;
@@ -12,8 +15,9 @@ export type TestSuiteConfig = {
export async function defaultCleanDatabase(connection: InstanceType<typeof PostgresConnection>) { export async function defaultCleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
const kysely = connection.kysely; const kysely = connection.kysely;
// drop all tables & create new schema // drop all tables+indexes & create new schema
await kysely.schema.dropSchema("public").ifExists().cascade().execute(); await kysely.schema.dropSchema("public").ifExists().cascade().execute();
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
await kysely.schema.createSchema("public").execute(); await kysely.schema.createSchema("public").execute();
} }
@@ -32,6 +36,23 @@ export function testSuite(config: TestSuiteConfig) {
beforeAll(() => disableConsoleLog(["log", "warn", "error"])); beforeAll(() => disableConsoleLog(["log", "warn", "error"]));
afterAll(() => enableConsoleLog()); afterAll(() => enableConsoleLog());
// @todo: postgres seems to add multiple indexes, thus failing the test suite
/* describe("test suite", () => {
connectionTestSuite(bunTestRunner, {
makeConnection: () => {
const connection = config.createConnection();
return {
connection,
dispose: async () => {
await cleanDatabase(connection, config);
await connection.close();
},
};
},
rawDialectDetails: [],
});
}); */
describe("base", () => { describe("base", () => {
it("should connect to the database", async () => { it("should connect to the database", async () => {
const connection = config.createConnection(); const connection = config.createConnection();
@@ -73,14 +94,14 @@ export function testSuite(config: TestSuiteConfig) {
}); });
it("should create a basic schema", async () => { it("should create a basic schema", async () => {
const schema = proto.em( const schema = em(
{ {
posts: proto.entity("posts", { posts: entity("posts", {
title: proto.text().required(), title: text().required(),
content: proto.text(), content: text(),
}), }),
comments: proto.entity("comments", { comments: entity("comments", {
content: proto.text(), content: text(),
}), }),
}, },
(fns, s) => { (fns, s) => {
@@ -153,20 +174,20 @@ export function testSuite(config: TestSuiteConfig) {
}); });
it("should support uuid", async () => { it("should support uuid", async () => {
const schema = proto.em( const schema = em(
{ {
posts: proto.entity( posts: entity(
"posts", "posts",
{ {
title: proto.text().required(), title: text().required(),
content: proto.text(), content: text(),
}, },
{ {
primary_format: "uuid", primary_format: "uuid",
}, },
), ),
comments: proto.entity("comments", { comments: entity("comments", {
content: proto.text(), content: text(),
}), }),
}, },
(fns, s) => { (fns, s) => {
@@ -187,8 +208,8 @@ export function testSuite(config: TestSuiteConfig) {
// @ts-expect-error // @ts-expect-error
expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid"); expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid");
const em = app.em; const $em = app.em;
const mutator = em.mutator(em.entity("posts")); const mutator = $em.mutator($em.entity("posts"));
const data = await mutator.insertOne({ title: "Hello", content: "World" }); const data = await mutator.insertOne({ title: "Hello", content: "World" });
expect(data.data.id).toBeString(); expect(data.data.id).toBeString();
expect(String(data.data.id).length).toBe(36); expect(String(data.data.id).length).toBe(36);

View File

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