mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
Merge remote-tracking branch 'origin/release/0.20' into feat/opfs-and-sqlocal
This commit is contained in:
@@ -1,102 +0,0 @@
|
||||
# Postgres adapter for `bknd` (experimental)
|
||||
This packages adds an adapter to use a Postgres database with [`bknd`](https://github.com/bknd-io/bknd). It works with both `pg` and `postgres` drivers, and supports custom postgres connections.
|
||||
* works with any Postgres database (tested with Supabase, Neon, Xata, and RDS)
|
||||
* choose between `pg` and `postgres` drivers
|
||||
* create custom postgres connections with any kysely postgres dialect
|
||||
|
||||
## Installation
|
||||
Install the adapter with:
|
||||
```bash
|
||||
npm install @bknd/postgres
|
||||
```
|
||||
|
||||
## Using `pg` driver
|
||||
Install the [`pg`](https://github.com/brianc/node-postgres) driver with:
|
||||
```bash
|
||||
npm install pg
|
||||
```
|
||||
|
||||
Create a connection:
|
||||
|
||||
```ts
|
||||
import { pg } from "@bknd/postgres";
|
||||
|
||||
// accepts `pg` configuration
|
||||
const connection = pg({
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
user: "postgres",
|
||||
password: "postgres",
|
||||
database: "postgres",
|
||||
});
|
||||
|
||||
// or with a connection string
|
||||
const connection = pg({
|
||||
connectionString: "postgres://postgres:postgres@localhost:5432/postgres",
|
||||
});
|
||||
```
|
||||
|
||||
## Using `postgres` driver
|
||||
|
||||
Install the [`postgres`](https://github.com/porsager/postgres) driver with:
|
||||
```bash
|
||||
npm install postgres
|
||||
```
|
||||
|
||||
Create a connection:
|
||||
|
||||
```ts
|
||||
import { postgresJs } from "@bknd/postgres";
|
||||
|
||||
// accepts `postgres` configuration
|
||||
const connection = postgresJs("postgres://postgres:postgres@localhost:5432/postgres");
|
||||
```
|
||||
|
||||
## Using custom postgres dialects
|
||||
|
||||
You can create a custom kysely postgres dialect by using the `createCustomPostgresConnection` function.
|
||||
|
||||
```ts
|
||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
||||
|
||||
const connection = createCustomPostgresConnection("my_postgres_dialect", MyDialect)({
|
||||
// your custom dialect configuration
|
||||
supports: {
|
||||
batching: true
|
||||
},
|
||||
excludeTables: ["my_table"],
|
||||
plugins: [new MyKyselyPlugin()],
|
||||
});
|
||||
```
|
||||
|
||||
### Custom `neon` connection
|
||||
|
||||
```typescript
|
||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
||||
import { NeonDialect } from "kysely-neon";
|
||||
|
||||
const connection = createCustomPostgresConnection("neon", NeonDialect)({
|
||||
connectionString: process.env.NEON,
|
||||
});
|
||||
```
|
||||
|
||||
### Custom `xata` connection
|
||||
|
||||
```typescript
|
||||
import { createCustomPostgresConnection } from "@bknd/postgres";
|
||||
import { XataDialect } from "@xata.io/kysely";
|
||||
import { buildClient } from "@xata.io/client";
|
||||
|
||||
const client = buildClient();
|
||||
const xata = new client({
|
||||
databaseURL: process.env.XATA_URL,
|
||||
apiKey: process.env.XATA_API_KEY,
|
||||
branch: process.env.XATA_BRANCH,
|
||||
});
|
||||
|
||||
const connection = createCustomPostgresConnection("xata", XataDialect, {
|
||||
supports: {
|
||||
batching: false,
|
||||
},
|
||||
})({ xata });
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
import { serve } from "bknd/adapter/bun";
|
||||
import { createCustomPostgresConnection } from "../src";
|
||||
import { NeonDialect } from "kysely-neon";
|
||||
|
||||
const neon = createCustomPostgresConnection(NeonDialect);
|
||||
|
||||
export default serve({
|
||||
connection: neon({
|
||||
connectionString: process.env.NEON,
|
||||
}),
|
||||
// ignore this, it's only required within this repository
|
||||
// because bknd is installed via "workspace:*"
|
||||
distPath: "../../app/dist",
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { serve } from "bknd/adapter/bun";
|
||||
import { createCustomPostgresConnection } from "../src";
|
||||
import { XataDialect } from "@xata.io/kysely";
|
||||
import { buildClient } from "@xata.io/client";
|
||||
|
||||
const client = buildClient();
|
||||
const xata = new client({
|
||||
databaseURL: process.env.XATA_URL,
|
||||
apiKey: process.env.XATA_API_KEY,
|
||||
branch: process.env.XATA_BRANCH,
|
||||
});
|
||||
|
||||
const connection = createCustomPostgresConnection(XataDialect, {
|
||||
supports: {
|
||||
batching: false,
|
||||
},
|
||||
})({ xata });
|
||||
|
||||
export default serve({
|
||||
connection,
|
||||
// ignore this, it's only required within this repository
|
||||
// because bknd is installed via "workspace:*"
|
||||
distPath: "../../../app/dist",
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "@bknd/postgres",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"test": "bun test",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"updater": "bun x npm-check-updates -ui",
|
||||
"prepublishOnly": "bun run typecheck && bun run test && bun run build",
|
||||
"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"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"kysely": "^0.27.6",
|
||||
"kysely-postgres-js": "^2.0.0",
|
||||
"pg": "^8.14.0",
|
||||
"postgres": "^3.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.5",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/pg": "^8.11.11",
|
||||
"@xata.io/client": "^0.0.0-next.v93343b9646f57a1e5c51c35eccf0767c2bb80baa",
|
||||
"@xata.io/kysely": "^0.2.1",
|
||||
"bknd": "workspace:*",
|
||||
"kysely-neon": "^1.3.0",
|
||||
"tsup": "^8.4.0"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": ["src/index.ts"],
|
||||
"format": ["esm"],
|
||||
"target": "es2022",
|
||||
"metafile": true,
|
||||
"clean": true,
|
||||
"minify": true,
|
||||
"dts": true,
|
||||
"external": ["bknd", "pg", "postgres", "kysely", "kysely-postgres-js"]
|
||||
},
|
||||
"files": ["dist", "README.md", "!*.map", "!metafile*.json"]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Kysely, PostgresDialect } from "kysely";
|
||||
import { PostgresIntrospector } from "./PostgresIntrospector";
|
||||
import { PostgresConnection, plugins } from "./PostgresConnection";
|
||||
import { customIntrospector } from "bknd";
|
||||
import $pg from "pg";
|
||||
|
||||
export type PgPostgresConnectionConfig = $pg.PoolConfig;
|
||||
|
||||
export class PgPostgresConnection extends PostgresConnection {
|
||||
override name = "pg";
|
||||
private pool: $pg.Pool;
|
||||
|
||||
constructor(config: PgPostgresConnectionConfig) {
|
||||
const pool = new $pg.Pool(config);
|
||||
const kysely = new Kysely({
|
||||
dialect: customIntrospector(PostgresDialect, PostgresIntrospector, {
|
||||
excludeTables: [],
|
||||
}).create({ pool }),
|
||||
plugins,
|
||||
});
|
||||
|
||||
super(kysely);
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
override async close(): Promise<void> {
|
||||
await this.pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
export function pg(config: PgPostgresConnectionConfig): PgPostgresConnection {
|
||||
return new PgPostgresConnection(config);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import {
|
||||
Connection,
|
||||
type DbFunctions,
|
||||
type FieldSpec,
|
||||
type SchemaResponse,
|
||||
type ConnQuery,
|
||||
type ConnQueryResults,
|
||||
} from "bknd";
|
||||
import {
|
||||
ParseJSONResultsPlugin,
|
||||
type ColumnDataType,
|
||||
type ColumnDefinitionBuilder,
|
||||
type Kysely,
|
||||
type KyselyPlugin,
|
||||
type SelectQueryBuilder,
|
||||
} from "kysely";
|
||||
import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/postgres";
|
||||
|
||||
export type QB = SelectQueryBuilder<any, any, any>;
|
||||
|
||||
export const plugins = [new ParseJSONResultsPlugin()];
|
||||
|
||||
export abstract class PostgresConnection extends Connection {
|
||||
protected override readonly supported = {
|
||||
batching: true,
|
||||
softscans: true,
|
||||
};
|
||||
|
||||
constructor(kysely: Kysely<any>, fn?: Partial<DbFunctions>, _plugins?: KyselyPlugin[]) {
|
||||
super(
|
||||
kysely,
|
||||
fn ?? {
|
||||
jsonArrayFrom,
|
||||
jsonBuildObject,
|
||||
jsonObjectFrom,
|
||||
},
|
||||
_plugins ?? plugins,
|
||||
);
|
||||
}
|
||||
|
||||
override getFieldSchema(spec: FieldSpec): SchemaResponse {
|
||||
this.validateFieldSpecType(spec.type);
|
||||
let type: ColumnDataType = spec.type;
|
||||
|
||||
if (spec.primary) {
|
||||
if (spec.type === "integer") {
|
||||
type = "serial";
|
||||
}
|
||||
}
|
||||
|
||||
switch (spec.type) {
|
||||
case "blob":
|
||||
type = "bytea";
|
||||
break;
|
||||
case "date":
|
||||
case "datetime":
|
||||
// https://www.postgresql.org/docs/17/datatype-datetime.html
|
||||
type = "timestamp";
|
||||
break;
|
||||
case "text":
|
||||
// https://www.postgresql.org/docs/17/datatype-character.html
|
||||
type = "varchar";
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
spec.name,
|
||||
type,
|
||||
(col: ColumnDefinitionBuilder) => {
|
||||
if (spec.primary) {
|
||||
return col.primaryKey();
|
||||
}
|
||||
if (spec.references) {
|
||||
return col
|
||||
.references(spec.references)
|
||||
.onDelete(spec.onDelete ?? "set null")
|
||||
.onUpdate(spec.onUpdate ?? "no action");
|
||||
}
|
||||
return spec.nullable ? col : col.notNull();
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
|
||||
return this.kysely.transaction().execute(async (trx) => {
|
||||
return Promise.all(qbs.map((q) => trx.executeQuery(q)));
|
||||
}) as any;
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { type SchemaMetadata, sql } from "kysely";
|
||||
import { BaseIntrospector } from "bknd";
|
||||
|
||||
type PostgresSchemaSpec = {
|
||||
name: string;
|
||||
type: "VIEW" | "BASE TABLE";
|
||||
columns: {
|
||||
name: string;
|
||||
type: string;
|
||||
notnull: number;
|
||||
dflt: string;
|
||||
pk: boolean;
|
||||
}[];
|
||||
indices: {
|
||||
name: string;
|
||||
origin: string;
|
||||
partial: number;
|
||||
sql: string;
|
||||
columns: { name: string; seqno: number }[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export class PostgresIntrospector extends BaseIntrospector {
|
||||
async getSchemas(): Promise<SchemaMetadata[]> {
|
||||
const rawSchemas = await this.db
|
||||
.selectFrom("pg_catalog.pg_namespace")
|
||||
.select("nspname")
|
||||
.$castTo<{ nspname: string }>()
|
||||
.execute();
|
||||
|
||||
return rawSchemas.map((it) => ({ name: it.nspname }));
|
||||
}
|
||||
|
||||
async getSchemaSpec() {
|
||||
const query = sql`
|
||||
WITH tables_and_views AS (
|
||||
SELECT table_name AS name,
|
||||
table_type AS type
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type IN ('BASE TABLE', 'VIEW')
|
||||
AND table_name NOT LIKE 'pg_%'
|
||||
AND table_name NOT IN (${this.getExcludedTableNames().join(", ")})
|
||||
),
|
||||
|
||||
columns_info AS (
|
||||
SELECT table_name AS name,
|
||||
json_agg(json_build_object(
|
||||
'name', column_name,
|
||||
'type', data_type,
|
||||
'notnull', (CASE WHEN is_nullable = 'NO' THEN true ELSE false END),
|
||||
'dflt', column_default,
|
||||
'pk', (SELECT COUNT(*) > 0
|
||||
FROM information_schema.table_constraints tc
|
||||
INNER JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_name = c.table_name
|
||||
AND tc.constraint_type = 'PRIMARY KEY'
|
||||
AND kcu.column_name = c.column_name)
|
||||
)) AS columns
|
||||
FROM information_schema.columns c
|
||||
WHERE table_schema = 'public'
|
||||
GROUP BY table_name
|
||||
),
|
||||
|
||||
indices_info AS (
|
||||
SELECT
|
||||
t.relname AS table_name,
|
||||
json_agg(json_build_object(
|
||||
'name', i.relname,
|
||||
'origin', pg_get_indexdef(i.oid),
|
||||
'partial', (CASE WHEN ix.indisvalid THEN false ELSE true END),
|
||||
'sql', pg_get_indexdef(i.oid),
|
||||
'columns', (
|
||||
SELECT json_agg(json_build_object(
|
||||
'name', a.attname,
|
||||
'seqno', x.ordinal_position
|
||||
))
|
||||
FROM unnest(ix.indkey) WITH ORDINALITY AS x(attnum, ordinal_position)
|
||||
JOIN pg_attribute a ON a.attnum = x.attnum AND a.attrelid = t.oid
|
||||
))) AS indices
|
||||
FROM pg_class t
|
||||
LEFT JOIN pg_index ix ON t.oid = ix.indrelid
|
||||
LEFT JOIN pg_class i ON i.oid = ix.indexrelid
|
||||
WHERE t.relkind IN ('r', 'v') -- r = table, v = view
|
||||
AND t.relname NOT LIKE 'pg_%'
|
||||
GROUP BY t.relname
|
||||
)
|
||||
|
||||
SELECT
|
||||
tv.name,
|
||||
tv.type,
|
||||
ci.columns,
|
||||
ii.indices
|
||||
FROM tables_and_views tv
|
||||
LEFT JOIN columns_info ci ON tv.name = ci.name
|
||||
LEFT JOIN indices_info ii ON tv.name = ii.table_name;
|
||||
`;
|
||||
|
||||
const tables = await this.executeWithPlugins<PostgresSchemaSpec[]>(query);
|
||||
|
||||
return tables.map((table) => ({
|
||||
name: table.name,
|
||||
isView: table.type === "VIEW",
|
||||
columns: table.columns.map((col) => {
|
||||
return {
|
||||
name: col.name,
|
||||
dataType: col.type,
|
||||
isNullable: !col.notnull,
|
||||
// @todo: check default value on 'nextval' see https://www.postgresql.org/docs/17/datatype-numeric.html#DATATYPE-SERIAL
|
||||
isAutoIncrementing: true, // just for now
|
||||
hasDefaultValue: col.dflt != null,
|
||||
comment: undefined,
|
||||
};
|
||||
}),
|
||||
indices: table.indices.map((index) => ({
|
||||
name: index.name,
|
||||
table: table.name,
|
||||
isUnique: index.sql?.match(/unique/i) != null,
|
||||
columns: index.columns.map((col) => ({
|
||||
name: col.name,
|
||||
order: col.seqno,
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Kysely } from "kysely";
|
||||
import { PostgresIntrospector } from "./PostgresIntrospector";
|
||||
import { PostgresConnection, plugins } from "./PostgresConnection";
|
||||
import { customIntrospector } from "bknd";
|
||||
import { PostgresJSDialect } from "kysely-postgres-js";
|
||||
import $postgresJs, { type Sql, type Options, type PostgresType } from "postgres";
|
||||
|
||||
export type PostgresJsConfig = Options<Record<string, PostgresType>>;
|
||||
|
||||
export class PostgresJsConnection extends PostgresConnection {
|
||||
override name = "postgres-js";
|
||||
|
||||
private postgres: Sql;
|
||||
|
||||
constructor(opts: { postgres: Sql }) {
|
||||
const kysely = new Kysely({
|
||||
dialect: customIntrospector(PostgresJSDialect, PostgresIntrospector, {
|
||||
excludeTables: [],
|
||||
}).create({ postgres: opts.postgres }),
|
||||
plugins,
|
||||
});
|
||||
|
||||
super(kysely);
|
||||
this.postgres = opts.postgres;
|
||||
}
|
||||
|
||||
override async close(): Promise<void> {
|
||||
await this.postgres.end();
|
||||
}
|
||||
}
|
||||
|
||||
export function postgresJs(
|
||||
connectionString: string,
|
||||
config?: PostgresJsConfig,
|
||||
): PostgresJsConnection;
|
||||
export function postgresJs(config: PostgresJsConfig): PostgresJsConnection;
|
||||
export function postgresJs(
|
||||
first: PostgresJsConfig | string,
|
||||
second?: PostgresJsConfig,
|
||||
): PostgresJsConnection {
|
||||
const postgres = typeof first === "string" ? $postgresJs(first, second) : $postgresJs(first);
|
||||
return new PostgresJsConnection({ postgres });
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { customIntrospector, type DbFunctions } from "bknd";
|
||||
import { Kysely, type Dialect, type KyselyPlugin } from "kysely";
|
||||
import { plugins, PostgresConnection } from "./PostgresConnection";
|
||||
import { PostgresIntrospector } from "./PostgresIntrospector";
|
||||
|
||||
export type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
export type CustomPostgresConnection = {
|
||||
supports?: PostgresConnection["supported"];
|
||||
fn?: Partial<DbFunctions>;
|
||||
plugins?: KyselyPlugin[];
|
||||
excludeTables?: string[];
|
||||
};
|
||||
|
||||
export function createCustomPostgresConnection<
|
||||
T extends Constructor<Dialect>,
|
||||
C extends ConstructorParameters<T>[0],
|
||||
>(
|
||||
name: string,
|
||||
dialect: Constructor<Dialect>,
|
||||
options?: CustomPostgresConnection,
|
||||
): (config: C) => PostgresConnection {
|
||||
const supported = {
|
||||
batching: true,
|
||||
...((options?.supports ?? {}) as any),
|
||||
};
|
||||
|
||||
return (config: C) =>
|
||||
new (class extends PostgresConnection {
|
||||
override name = name;
|
||||
override readonly supported = supported;
|
||||
|
||||
constructor(config: C) {
|
||||
super(
|
||||
new Kysely({
|
||||
dialect: customIntrospector(dialect, PostgresIntrospector, {
|
||||
excludeTables: options?.excludeTables ?? [],
|
||||
}).create(config),
|
||||
plugins: options?.plugins ?? plugins,
|
||||
}),
|
||||
options?.fn,
|
||||
options?.plugins,
|
||||
);
|
||||
}
|
||||
})(config);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export { pg, PgPostgresConnection, type PgPostgresConnectionConfig } from "./PgPostgresConnection";
|
||||
export { PostgresIntrospector } from "./PostgresIntrospector";
|
||||
export { PostgresConnection, type QB, plugins } from "./PostgresConnection";
|
||||
export { postgresJs, PostgresJsConnection, type PostgresJsConfig } from "./PostgresJsConnection";
|
||||
export { createCustomPostgresConnection } from "./custom";
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe } from "bun:test";
|
||||
import { pg } from "../src/PgPostgresConnection";
|
||||
import { testSuite } from "./suite";
|
||||
|
||||
describe("pg", () => {
|
||||
testSuite({
|
||||
createConnection: () =>
|
||||
pg({
|
||||
host: "localhost",
|
||||
port: 5430,
|
||||
user: "postgres",
|
||||
password: "postgres",
|
||||
database: "bknd",
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe } from "bun:test";
|
||||
import { postgresJs } from "../src/PostgresJsConnection";
|
||||
import { testSuite } from "./suite";
|
||||
|
||||
describe("postgresjs", () => {
|
||||
testSuite({
|
||||
createConnection: () =>
|
||||
postgresJs({
|
||||
host: "localhost",
|
||||
port: 5430,
|
||||
user: "postgres",
|
||||
password: "postgres",
|
||||
database: "bknd",
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -1,218 +0,0 @@
|
||||
import { describe, beforeAll, afterAll, expect, it, afterEach } from "bun:test";
|
||||
import type { PostgresConnection } from "../src";
|
||||
import { createApp, em, entity, text } from "bknd";
|
||||
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 = {
|
||||
createConnection: () => InstanceType<typeof PostgresConnection>;
|
||||
cleanDatabase?: (connection: InstanceType<typeof PostgresConnection>) => Promise<void>;
|
||||
};
|
||||
|
||||
export async function defaultCleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
|
||||
const kysely = connection.kysely;
|
||||
|
||||
// drop all tables+indexes & create new schema
|
||||
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
|
||||
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
|
||||
await kysely.schema.createSchema("public").execute();
|
||||
}
|
||||
|
||||
async function cleanDatabase(
|
||||
connection: InstanceType<typeof PostgresConnection>,
|
||||
config: TestSuiteConfig,
|
||||
) {
|
||||
if (config.cleanDatabase) {
|
||||
await config.cleanDatabase(connection);
|
||||
} else {
|
||||
await defaultCleanDatabase(connection);
|
||||
}
|
||||
}
|
||||
|
||||
export function testSuite(config: TestSuiteConfig) {
|
||||
beforeAll(() => disableConsoleLog(["log", "warn", "error"]));
|
||||
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", () => {
|
||||
it("should connect to the database", async () => {
|
||||
const connection = config.createConnection();
|
||||
expect(await connection.ping()).toBe(true);
|
||||
});
|
||||
|
||||
it("should clean the database", async () => {
|
||||
const connection = config.createConnection();
|
||||
await cleanDatabase(connection, config);
|
||||
|
||||
const tables = await connection.getIntrospector().getTables();
|
||||
expect(tables).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration", () => {
|
||||
let connection: PostgresConnection;
|
||||
beforeAll(async () => {
|
||||
connection = config.createConnection();
|
||||
await cleanDatabase(connection, config);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanDatabase(connection, config);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await connection.close();
|
||||
});
|
||||
|
||||
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 = em(
|
||||
{
|
||||
posts: entity("posts", {
|
||||
title: text().required(),
|
||||
content: text(),
|
||||
}),
|
||||
comments: entity("comments", {
|
||||
content: text(),
|
||||
}),
|
||||
},
|
||||
(fns, s) => {
|
||||
fns.relation(s.comments).manyToOne(s.posts);
|
||||
fns.index(s.posts).on(["title"], true);
|
||||
},
|
||||
);
|
||||
|
||||
const app = createApp({
|
||||
connection,
|
||||
config: {
|
||||
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);
|
||||
});
|
||||
|
||||
it("should support uuid", async () => {
|
||||
const schema = em(
|
||||
{
|
||||
posts: entity(
|
||||
"posts",
|
||||
{
|
||||
title: text().required(),
|
||||
content: text(),
|
||||
},
|
||||
{
|
||||
primary_format: "uuid",
|
||||
},
|
||||
),
|
||||
comments: entity("comments", {
|
||||
content: text(),
|
||||
}),
|
||||
},
|
||||
(fns, s) => {
|
||||
fns.relation(s.comments).manyToOne(s.posts);
|
||||
fns.index(s.posts).on(["title"], true);
|
||||
},
|
||||
);
|
||||
|
||||
const app = createApp({
|
||||
connection,
|
||||
config: {
|
||||
data: schema.toJSON(),
|
||||
},
|
||||
});
|
||||
|
||||
await app.build();
|
||||
const config = app.toJSON();
|
||||
// @ts-expect-error
|
||||
expect(config.data.entities?.posts.fields?.id.config?.format).toBe("uuid");
|
||||
|
||||
const $em = app.em;
|
||||
const mutator = $em.mutator($em.entity("posts"));
|
||||
const data = await mutator.insertOne({ title: "Hello", content: "World" });
|
||||
expect(data.data.id).toBeString();
|
||||
expect(String(data.data.id).length).toBe(36);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"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,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"$bknd/*": ["../../app/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user