diff --git a/app/build.ts b/app/build.ts index 0022a80..836a816 100644 --- a/app/build.ts +++ b/app/build.ts @@ -53,6 +53,9 @@ function banner(title: string) { console.log("-".repeat(40)); } +// collection of always-external packages +const external = ["bun:test", "@libsql/client"] as const; + /** * Building backend and general API */ @@ -64,7 +67,7 @@ async function buildApi() { watch, entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"], outDir: "dist", - external: ["bun:test", "@libsql/client"], + external: [...external], metafile: true, platform: "browser", format: ["esm"], @@ -93,7 +96,7 @@ async function buildUi() { sourcemap, watch, external: [ - "bun:test", + ...external, "react", "react-dom", "react/jsx-runtime", diff --git a/app/package.json b/app/package.json index 26c41d0..442c9a0 100644 --- a/app/package.json +++ b/app/package.json @@ -85,7 +85,6 @@ "kysely-d1": "^0.3.0", "open": "^10.1.0", "openapi-types": "^12.1.3", - "pg": "^8.13.3", "postcss": "^8.5.3", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", diff --git a/app/src/adapter/cloudflare/D1Connection.ts b/app/src/adapter/cloudflare/D1Connection.ts index 768ca44..5b4b059 100644 --- a/app/src/adapter/cloudflare/D1Connection.ts +++ b/app/src/adapter/cloudflare/D1Connection.ts @@ -18,6 +18,10 @@ class CustomD1Dialect extends D1Dialect { } export class D1Connection extends SqliteConnection { + protected override readonly supported = { + batching: true, + }; + constructor(private config: D1ConnectionConfig) { const plugins = [new ParseJSONResultsPlugin()]; @@ -28,14 +32,6 @@ export class D1Connection extends SqliteConnection { super(kysely, {}, plugins); } - override supportsBatching(): boolean { - return true; - } - - override supportsIndices(): boolean { - return true; - } - protected override async batch( queries: [...Queries], ): Promise<{ diff --git a/app/src/data/connection/Connection.ts b/app/src/data/connection/Connection.ts index 5757511..7eae588 100644 --- a/app/src/data/connection/Connection.ts +++ b/app/src/data/connection/Connection.ts @@ -2,7 +2,6 @@ import { type AliasableExpression, type ColumnBuilderCallback, type ColumnDataType, - type DatabaseIntrospector, type Expression, type Kysely, type KyselyPlugin, @@ -77,6 +76,9 @@ const CONN_SYMBOL = Symbol.for("bknd:connection"); export abstract class Connection { kysely: Kysely; + protected readonly supported = { + batching: false, + }; constructor( kysely: Kysely, @@ -101,13 +103,8 @@ export abstract class Connection { return this.kysely.introspection as any; } - supportsBatching(): boolean { - return false; - } - - // @todo: add if only first field is used in index - supportsIndices(): boolean { - return false; + supports(feature: keyof typeof this.supported): boolean { + return this.supported[feature] ?? false; } async ping(): Promise { @@ -129,7 +126,7 @@ export abstract class Connection { [K in keyof Queries]: Awaited>; }> { // bypass if no client support - if (!this.supportsBatching()) { + if (!this.supports("batching")) { const data: any = []; for (const q of queries) { const result = await q.execute(); @@ -151,5 +148,8 @@ export abstract class Connection { } abstract getFieldSchema(spec: FieldSpec, strict?: boolean): SchemaResponse; - abstract close(): Promise; + + async close(): Promise { + // no-op by default + } } diff --git a/app/src/data/connection/DummyConnection.ts b/app/src/data/connection/DummyConnection.ts index 9f7287a..d04d0af 100644 --- a/app/src/data/connection/DummyConnection.ts +++ b/app/src/data/connection/DummyConnection.ts @@ -1,6 +1,10 @@ import { Connection, type FieldSpec, type SchemaResponse } from "./Connection"; export class DummyConnection extends Connection { + protected override readonly supported = { + batching: true, + }; + constructor() { super(undefined as any); } diff --git a/app/src/data/connection/index.ts b/app/src/data/connection/index.ts index 155be94..2e745e0 100644 --- a/app/src/data/connection/index.ts +++ b/app/src/data/connection/index.ts @@ -1,12 +1,14 @@ -export { Connection } from "./Connection"; export { BaseIntrospector } from "./BaseIntrospector"; +export { + Connection, + type FieldSpec, + type IndexSpec, + type DbFunctions, + type SchemaResponse, +} from "./Connection"; // sqlite export { LibsqlConnection, type LibSqlCredentials } from "./sqlite/LibsqlConnection"; export { SqliteConnection } from "./sqlite/SqliteConnection"; -export { SqliteLocalConnection } from "./sqlite/SqliteLocalConnection"; export { SqliteIntrospector } from "./sqlite/SqliteIntrospector"; - -// postgres -export { PostgresConnection, type PostgresConnectionConfig } from "./postgres/PostgresConnection"; -export { PostgresIntrospector } from "./postgres/PostgresIntrospector"; +export { SqliteLocalConnection } from "./sqlite/SqliteLocalConnection"; diff --git a/app/src/data/connection/sqlite/LibsqlConnection.ts b/app/src/data/connection/sqlite/LibsqlConnection.ts index 9952054..895b6b0 100644 --- a/app/src/data/connection/sqlite/LibsqlConnection.ts +++ b/app/src/data/connection/sqlite/LibsqlConnection.ts @@ -1,8 +1,8 @@ import { type Client, type Config, type InStatement, createClient } from "@libsql/client"; import { LibsqlDialect } from "@libsql/kysely-libsql"; -import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely"; import { FilterNumericKeysPlugin } from "data/plugins/FilterNumericKeysPlugin"; import { KyselyPluginRunner } from "data/plugins/KyselyPluginRunner"; +import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely"; import type { QB } from "../Connection"; import { SqliteConnection } from "./SqliteConnection"; import { SqliteIntrospector } from "./SqliteIntrospector"; @@ -25,6 +25,9 @@ class CustomLibsqlDialect extends LibsqlDialect { export class LibsqlConnection extends SqliteConnection { private client: Client; + protected override readonly supported = { + batching: true, + }; constructor(client: Client); constructor(credentials: LibSqlCredentials); @@ -53,14 +56,6 @@ export class LibsqlConnection extends SqliteConnection { this.client = client; } - override supportsBatching(): boolean { - return true; - } - - override supportsIndices(): boolean { - return true; - } - getClient(): Client { return this.client; } diff --git a/app/src/data/connection/sqlite/SqliteConnection.ts b/app/src/data/connection/sqlite/SqliteConnection.ts index 9ef9091..a63d49b 100644 --- a/app/src/data/connection/sqlite/SqliteConnection.ts +++ b/app/src/data/connection/sqlite/SqliteConnection.ts @@ -16,10 +16,6 @@ export class SqliteConnection extends Connection { ); } - override supportsIndices(): boolean { - return true; - } - override getFieldSchema(spec: FieldSpec): SchemaResponse { this.validateFieldSpecType(spec.type); let type: ColumnDataType = spec.type; @@ -47,8 +43,4 @@ export class SqliteConnection extends Connection { }, ] as const; } - - override async close(): Promise { - // no-op - } } diff --git a/app/src/data/connection/sqlite/SqliteLocalConnection.ts b/app/src/data/connection/sqlite/SqliteLocalConnection.ts index 029808a..a92577b 100644 --- a/app/src/data/connection/sqlite/SqliteLocalConnection.ts +++ b/app/src/data/connection/sqlite/SqliteLocalConnection.ts @@ -1,5 +1,10 @@ -import { type DatabaseIntrospector, ParseJSONResultsPlugin, type SqliteDatabase } from "kysely"; -import { Kysely, SqliteDialect } from "kysely"; +import { + type DatabaseIntrospector, + Kysely, + ParseJSONResultsPlugin, + type SqliteDatabase, + SqliteDialect, +} from "kysely"; import { SqliteConnection } from "./SqliteConnection"; import { SqliteIntrospector } from "./SqliteIntrospector"; @@ -23,8 +28,4 @@ export class SqliteLocalConnection extends SqliteConnection { super(kysely, {}, plugins); } - - override supportsIndices(): boolean { - return true; - } } diff --git a/app/src/data/schema/SchemaManager.ts b/app/src/data/schema/SchemaManager.ts index a8d9df0..7ad1ba1 100644 --- a/app/src/data/schema/SchemaManager.ts +++ b/app/src/data/schema/SchemaManager.ts @@ -49,10 +49,6 @@ export class SchemaManager { constructor(private readonly em: EntityManager) {} private getIntrospector() { - if (!this.em.connection.supportsIndices()) { - throw new Error("Indices are not supported by the current connection"); - } - return this.em.connection.getIntrospector(); } diff --git a/bun.lock b/bun.lock index 8925666..6af2967 100644 --- a/bun.lock +++ b/bun.lock @@ -83,7 +83,6 @@ "kysely-d1": "^0.3.0", "open": "^10.1.0", "openapi-types": "^12.1.3", - "pg": "^8.13.3", "postcss": "^8.5.3", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", @@ -139,6 +138,22 @@ "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": { "@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/postgres": ["@bknd/postgres@workspace:packages/postgres"], + "@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=="], @@ -1171,6 +1188,8 @@ "@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/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=="], + "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], + "ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], "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-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-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=="], @@ -2661,13 +2684,15 @@ "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=="], @@ -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=="], + "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=="], "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=="], + "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/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], diff --git a/packages/postgres/README.md b/packages/postgres/README.md new file mode 100644 index 0000000..cb22856 --- /dev/null +++ b/packages/postgres/README.md @@ -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). \ No newline at end of file diff --git a/packages/postgres/package.json b/packages/postgres/package.json new file mode 100644 index 0000000..7f1247e --- /dev/null +++ b/packages/postgres/package.json @@ -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"] +} diff --git a/app/src/data/connection/postgres/PostgresConnection.ts b/packages/postgres/src/PostgresConnection.ts similarity index 80% rename from app/src/data/connection/postgres/PostgresConnection.ts rename to packages/postgres/src/PostgresConnection.ts index 0479369..c1495d5 100644 --- a/app/src/data/connection/postgres/PostgresConnection.ts +++ b/packages/postgres/src/PostgresConnection.ts @@ -1,31 +1,34 @@ +import { Connection, type FieldSpec, type SchemaResponse } from "bknd/data"; import { - Kysely, - PostgresDialect, - type DatabaseIntrospector, type ColumnDataType, type ColumnDefinitionBuilder, + type DatabaseIntrospector, + Kysely, ParseJSONResultsPlugin, + PostgresDialect, + type SelectQueryBuilder, } from "kysely"; +import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/postgres"; import pg from "pg"; import { PostgresIntrospector } from "./PostgresIntrospector"; -import { - type FieldSpec, - type SchemaResponse, - Connection, - type QB, -} from "data/connection/Connection"; export type PostgresConnectionConfig = pg.PoolConfig; +export type QB = SelectQueryBuilder; const plugins = [new ParseJSONResultsPlugin()]; class CustomPostgresDialect extends PostgresDialect { override createIntrospector(db: Kysely): DatabaseIntrospector { - return new PostgresIntrospector(db); + return new PostgresIntrospector(db, { + excludeTables: [], + }); } } export class PostgresConnection extends Connection { + protected override readonly supported = { + batching: true, + }; private pool: pg.Pool; constructor(config: PostgresConnectionConfig) { @@ -38,14 +41,18 @@ export class PostgresConnection extends Connection { //log: ["query", "error"], }); - super(kysely, {}, plugins); + super( + kysely, + { + jsonArrayFrom, + jsonBuildObject, + jsonObjectFrom, + }, + plugins, + ); this.pool = pool; } - override supportsIndices(): boolean { - return true; - } - override getFieldSchema(spec: FieldSpec): SchemaResponse { this.validateFieldSpecType(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 { await this.pool.end(); } diff --git a/app/src/data/connection/postgres/PostgresIntrospector.ts b/packages/postgres/src/PostgresIntrospector.ts similarity index 98% rename from app/src/data/connection/postgres/PostgresIntrospector.ts rename to packages/postgres/src/PostgresIntrospector.ts index 82d6aaa..82b75ba 100644 --- a/app/src/data/connection/postgres/PostgresIntrospector.ts +++ b/packages/postgres/src/PostgresIntrospector.ts @@ -1,5 +1,5 @@ import { type SchemaMetadata, sql } from "kysely"; -import { BaseIntrospector } from "data/connection/BaseIntrospector"; +import { BaseIntrospector } from "bknd/data"; type PostgresSchemaSpec = { name: string; diff --git a/packages/postgres/src/index.ts b/packages/postgres/src/index.ts new file mode 100644 index 0000000..ef7c56f --- /dev/null +++ b/packages/postgres/src/index.ts @@ -0,0 +1,2 @@ +export { PostgresConnection, type PostgresConnectionConfig } from "./PostgresConnection"; +export { PostgresIntrospector } from "./PostgresIntrospector"; diff --git a/packages/postgres/test/base.test.ts b/packages/postgres/test/base.test.ts new file mode 100644 index 0000000..c5adbda --- /dev/null +++ b/packages/postgres/test/base.test.ts @@ -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([]); + }); +}); diff --git a/packages/postgres/test/integration.test.ts b/packages/postgres/test/integration.test.ts new file mode 100644 index 0000000..90b8746 --- /dev/null +++ b/packages/postgres/test/integration.test.ts @@ -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); + }); +}); diff --git a/packages/postgres/test/setup.ts b/packages/postgres/test/setup.ts new file mode 100644 index 0000000..d82427d --- /dev/null +++ b/packages/postgres/test/setup.ts @@ -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(); +} diff --git a/packages/postgres/tsconfig.json b/packages/postgres/tsconfig.json new file mode 100644 index 0000000..d2359e0 --- /dev/null +++ b/packages/postgres/tsconfig.json @@ -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"] +}