initialized postgres support

This commit is contained in:
dswbx
2025-03-07 15:02:19 +01:00
parent 8550aef606
commit a5c422d45d
30 changed files with 759 additions and 220 deletions

View File

@@ -1,9 +1,9 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, afterEach, describe, expect, test } from "bun:test";
import { App } from "../src"; import { App } from "../src";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterAll(afterAllCleanup); afterEach(afterAllCleanup);
describe("App tests", async () => { describe("App tests", async () => {
test("boots and pongs", async () => { test("boots and pongs", async () => {
@@ -12,4 +12,16 @@ describe("App tests", async () => {
//expect(await app.data?.em.ping()).toBeTrue(); //expect(await app.data?.em.ping()).toBeTrue();
}); });
/*test.only("what", async () => {
const app = new App(dummyConnection, {
auth: {
enabled: true,
},
});
await app.module.auth.build();
await app.module.data.build();
console.log(app.em.entities.map((e) => e.name));
console.log(await app.em.schema().getDiff());
});*/
}); });

View File

@@ -0,0 +1,53 @@
import { describe, expect, test } from "bun:test";
import { createApp } from "App";
import { PostgresConnection } from "data/connection/postgres/PostgresConnection";
import * as proto from "data/prototype";
import { PostgresIntrospector } from "data/connection/postgres/PostgresIntrospector";
import { ParseJSONResultsPlugin } from "kysely";
const connection = new PostgresConnection({
database: "test",
host: "localhost",
user: "root",
password: "1234",
port: 5433,
});
describe("postgres", () => {
test.skip("introspector", async () => {
const introspector = new PostgresIntrospector(connection.kysely, {
plugins: [new ParseJSONResultsPlugin()],
});
console.log(await introspector.getSchema());
});
test("builds", async () => {
const schema = proto.em(
{
posts: proto.entity("posts", {
title: proto.text().required(),
}),
comments: proto.entity("comments", {
text: proto.text(),
}),
},
(ctx, s) => {
ctx.relation(s.comments).manyToOne(s.posts);
ctx.index(s.posts).on(["title"], true);
ctx.index(s.comments).on(["text"]);
},
);
const app = createApp({
initialConfig: {
data: schema.toJSON(),
},
connection,
});
await app.build({ sync: true });
expect(app.version()).toBeDefined();
});
});

View File

@@ -27,7 +27,7 @@ describe("Relations", async () => {
const sql1 = schema const sql1 = schema
.createTable("posts") .createTable("posts")
.addColumn(...r1.schema()!) .addColumn(...em.connection.getFieldSchema(r1.schema())!)
.compile().sql; .compile().sql;
expect(sql1).toBe( expect(sql1).toBe(
@@ -43,7 +43,7 @@ describe("Relations", async () => {
const sql2 = schema const sql2 = schema
.createTable("posts") .createTable("posts")
.addColumn(...r2.schema()!) .addColumn(...em.connection.getFieldSchema(r2.schema())!)
.compile().sql; .compile().sql;
expect(sql2).toBe( expect(sql2).toBe(

View File

@@ -15,7 +15,7 @@ describe("SchemaManager tests", async () => {
const em = new EntityManager([entity], dummyConnection, [], [index]); const em = new EntityManager([entity], dummyConnection, [], [index]);
const schema = new SchemaManager(em); const schema = new SchemaManager(em);
const introspection = schema.getIntrospectionFromEntity(em.entities[0]); const introspection = schema.getIntrospectionFromEntity(em.entities[0]!);
expect(introspection).toEqual({ expect(introspection).toEqual({
name: "test", name: "test",
isView: false, isView: false,
@@ -109,7 +109,7 @@ describe("SchemaManager tests", async () => {
await schema.sync({ force: true, drop: true }); await schema.sync({ force: true, drop: true });
const diffAfter = await schema.getDiff(); const diffAfter = await schema.getDiff();
console.log("diffAfter", diffAfter); //console.log("diffAfter", diffAfter);
expect(diffAfter.length).toBe(0); expect(diffAfter.length).toBe(0);
await kysely.schema.dropTable(table).execute(); await kysely.schema.dropTable(table).execute();

View File

@@ -0,0 +1,108 @@
import { beforeEach, describe, test, expect } from "bun:test";
import { SqliteIntrospector } from "data/connection/SqliteIntrospector";
import { getDummyConnection, getDummyDatabase } from "../../helper";
import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from "kysely";
import { _jsonp } from "core/utils";
function create() {
const database = getDummyDatabase().dummyDb;
return new Kysely({
dialect: new SqliteDialect({ database }),
});
}
function createLibsql() {
const database = getDummyDatabase().dummyDb;
return new Kysely({
dialect: new SqliteDialect({ database }),
});
}
describe("SqliteIntrospector", () => {
test("asdf", async () => {
const kysely = create();
await kysely.schema
.createTable("test")
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
.addColumn("string", "text", (col) => col.notNull())
.addColumn("number", "integer")
.execute();
await kysely.schema
.createIndex("idx_test_string")
.on("test")
.columns(["string"])
.unique()
.execute();
await kysely.schema
.createTable("test2")
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
.addColumn("number", "integer")
.execute();
await kysely.schema.createIndex("idx_test2_number").on("test2").columns(["number"]).execute();
const introspector = new SqliteIntrospector(kysely, {});
const result = await introspector.getTables();
//console.log(_jsonp(result));
expect(result).toEqual([
{
name: "test",
isView: false,
columns: [
{
name: "id",
dataType: "INTEGER",
isNullable: false,
isAutoIncrementing: true,
hasDefaultValue: false,
comment: undefined,
},
{
name: "string",
dataType: "TEXT",
isNullable: false,
isAutoIncrementing: false,
hasDefaultValue: false,
comment: undefined,
},
{
comment: undefined,
dataType: "INTEGER",
hasDefaultValue: false,
isAutoIncrementing: false,
isNullable: true,
name: "number",
},
],
},
{
name: "test2",
isView: false,
columns: [
{
name: "id",
dataType: "INTEGER",
isNullable: false,
isAutoIncrementing: true,
hasDefaultValue: false,
comment: undefined,
},
{
name: "number",
dataType: "INTEGER",
isNullable: true,
isAutoIncrementing: false,
hasDefaultValue: false,
comment: undefined,
},
],
},
]);
});
});

View File

@@ -1,23 +1,29 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Default, parse, stripMark } from "../../../../src/core/utils"; import { Default, stripMark } from "../../../../src/core/utils";
import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data"; import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
import { runBaseFieldTests, transformPersist } from "./inc"; import { runBaseFieldTests } from "./inc";
describe("[data] Field", async () => { describe("[data] Field", async () => {
class FieldSpec extends Field { class FieldSpec extends Field {
schema(): SchemaResponse {
return this.useSchemaHelper("text");
}
getSchema() { getSchema() {
return baseFieldConfigSchema; return baseFieldConfigSchema;
} }
} }
test("fieldSpec", () => {
expect(new FieldSpec("test").schema()).toEqual({
name: "test",
type: "text",
nullable: true, // always true
dflt: undefined, // never using default value
});
});
runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" }); runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" });
test("default config", async () => { test("default config", async () => {
const config = Default(baseFieldConfigSchema, {}); const config = Default(baseFieldConfigSchema, {});
expect(stripMark(new FieldSpec("test").config)).toEqual(config); expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
}); });
test("transformPersist (specific)", async () => { test("transformPersist (specific)", async () => {

View File

@@ -10,7 +10,12 @@ describe("[data] PrimaryField", async () => {
test("schema", () => { test("schema", () => {
expect(field.name).toBe("primary"); expect(field.name).toBe("primary");
expect(field.schema()).toEqual(["primary", "integer", expect.any(Function)]); expect(field.schema()).toEqual({
name: "primary",
type: "integer" as const,
nullable: false,
primary: true,
});
}); });
test("hasDefault", async () => { test("hasDefault", async () => {

View File

@@ -34,11 +34,14 @@ export function runBaseFieldTests(
test("schema", () => { test("schema", () => {
expect(noConfigField.name).toBe("no_config"); expect(noConfigField.name).toBe("no_config");
expect(noConfigField.schema(null as any)).toEqual([
"no_config", const { type, name, nullable, dflt } = noConfigField.schema()!;
config.schemaType, expect({ type, name, nullable, dflt }).toEqual({
expect.any(Function), type: config.schemaType as any,
]); name: "no_config",
nullable: true, // always true
dflt: undefined, // never using default value
});
}); });
test("hasDefault", async () => { test("hasDefault", async () => {

View File

@@ -1,9 +1,12 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
import { App, createApp } from "../../src"; import { App, createApp } from "../../src";
import type { AuthResponse } from "../../src/auth"; import type { AuthResponse } from "../../src/auth";
import { auth } from "../../src/auth/middlewares"; import { auth } from "../../src/auth/middlewares";
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils"; import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
import { disableConsoleLog, enableConsoleLog } from "../helper"; import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterEach(afterAllCleanup);
beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -64,6 +67,7 @@ const configs = {
function createAuthApp() { function createAuthApp() {
const app = createApp({ const app = createApp({
connection: dummyConnection,
initialConfig: { initialConfig: {
auth: configs.auth, auth: configs.auth,
}, },

View File

@@ -63,16 +63,17 @@
"@aws-sdk/client-s3": "^3.613.0", "@aws-sdk/client-s3": "^3.613.0",
"@bluwy/giget-core": "^0.1.2", "@bluwy/giget-core": "^0.1.2",
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@mantine/modals": "^7.13.4",
"@mantine/notifications": "^7.13.4",
"@hono/typebox-validator": "^0.2.6", "@hono/typebox-validator": "^0.2.6",
"@hono/vite-dev-server": "^0.17.0", "@hono/vite-dev-server": "^0.17.0",
"@hono/zod-validator": "^0.4.1", "@hono/zod-validator": "^0.4.1",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@libsql/kysely-libsql": "^0.4.1", "@libsql/kysely-libsql": "^0.4.1",
"@mantine/modals": "^7.13.4",
"@mantine/notifications": "^7.13.4",
"@rjsf/core": "5.22.2", "@rjsf/core": "5.22.2",
"@tabler/icons-react": "3.18.0", "@tabler/icons-react": "3.18.0",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/pg": "^8.11.11",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.3",
@@ -84,6 +85,7 @@
"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.4.47", "postcss": "^8.4.47",
"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

@@ -1,9 +1,12 @@
import { import {
type AliasableExpression, type AliasableExpression,
type ColumnBuilderCallback,
type ColumnDataType,
type DatabaseIntrospector, type DatabaseIntrospector,
type Expression, type Expression,
type Kysely, type Kysely,
type KyselyPlugin, type KyselyPlugin,
type OnModifyForeignAction,
type RawBuilder, type RawBuilder,
type SelectQueryBuilder, type SelectQueryBuilder,
type SelectQueryNode, type SelectQueryNode,
@@ -29,6 +32,38 @@ export interface SelectQueryBuilderExpression<O> extends AliasableExpression<O>
toOperationNode(): SelectQueryNode; toOperationNode(): SelectQueryNode;
} }
export type SchemaResponse = [string, ColumnDataType, ColumnBuilderCallback] | undefined;
const FieldSpecTypes = [
"text",
"integer",
"real",
"blob",
"date",
"datetime",
"timestamp",
"boolean",
"json",
] as const;
export type FieldSpec = {
type: (typeof FieldSpecTypes)[number];
name: string;
nullable?: boolean;
dflt?: any;
unique?: boolean;
primary?: boolean;
references?: string;
onDelete?: OnModifyForeignAction;
onUpdate?: OnModifyForeignAction;
};
export type IndexSpec = {
name: string;
columns: string[];
unique?: boolean;
};
export type DbFunctions = { export type DbFunctions = {
jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O> | null>; jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O> | null>;
jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O>[]>; jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O>[]>;
@@ -108,4 +143,15 @@ export abstract class Connection<DB = any> {
return await this.batch(queries); return await this.batch(queries);
} }
protected validateFieldSpecType(type: string): type is FieldSpec["type"] {
if (!FieldSpecTypes.includes(type as any)) {
throw new Error(
`Invalid field type "${type}". Allowed types are: ${FieldSpecTypes.join(", ")}`,
);
}
return true;
}
abstract getFieldSchema(spec: FieldSpec, strict?: boolean): SchemaResponse;
} }

View File

@@ -12,10 +12,13 @@ export type LibSqlCredentials = Config & {
protocol?: (typeof LIBSQL_PROTOCOLS)[number]; protocol?: (typeof LIBSQL_PROTOCOLS)[number];
}; };
const plugins = [new FilterNumericKeysPlugin(), new ParseJSONResultsPlugin()];
class CustomLibsqlDialect extends LibsqlDialect { class CustomLibsqlDialect extends LibsqlDialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector { override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new SqliteIntrospector(db, { return new SqliteIntrospector(db, {
excludeTables: ["libsql_wasm_func_table"], excludeTables: ["libsql_wasm_func_table"],
plugins,
}); });
} }
} }
@@ -26,7 +29,6 @@ export class LibsqlConnection extends SqliteConnection {
constructor(client: Client); constructor(client: Client);
constructor(credentials: LibSqlCredentials); constructor(credentials: LibSqlCredentials);
constructor(clientOrCredentials: Client | LibSqlCredentials) { constructor(clientOrCredentials: Client | LibSqlCredentials) {
const plugins = [new FilterNumericKeysPlugin(), new ParseJSONResultsPlugin()];
let client: Client; let client: Client;
if (clientOrCredentials && "url" in clientOrCredentials) { if (clientOrCredentials && "url" in clientOrCredentials) {
let { url, authToken, protocol } = clientOrCredentials; let { url, authToken, protocol } = clientOrCredentials;

View File

@@ -1,6 +1,6 @@
import type { Kysely, KyselyPlugin } from "kysely"; import type { ColumnDataType, ColumnDefinitionBuilder, Kysely, KyselyPlugin } from "kysely";
import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite"; import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite";
import { Connection, type DbFunctions } from "./Connection"; import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } from "./Connection";
export class SqliteConnection extends Connection { export class SqliteConnection extends Connection {
constructor(kysely: Kysely<any>, fn: Partial<DbFunctions> = {}, plugins: KyselyPlugin[] = []) { constructor(kysely: Kysely<any>, fn: Partial<DbFunctions> = {}, plugins: KyselyPlugin[] = []) {
@@ -19,4 +19,32 @@ export class SqliteConnection extends Connection {
override supportsIndices(): boolean { override supportsIndices(): boolean {
return true; return true;
} }
override getFieldSchema(spec: FieldSpec): SchemaResponse {
this.validateFieldSpecType(spec.type);
let type: ColumnDataType = spec.type;
switch (spec.type) {
case "json":
type = "text";
break;
}
return [
spec.name,
type,
(col: ColumnDefinitionBuilder) => {
if (spec.primary) {
return col.primaryKey().notNull().autoIncrement();
}
if (spec.references) {
let relCol = col.references(spec.references);
if (spec.onDelete) relCol = relCol.onDelete(spec.onDelete);
if (spec.onUpdate) relCol = relCol.onUpdate(spec.onUpdate);
return relCol;
}
return spec.nullable ? col : col.notNull();
},
] as const;
}
} }

View File

@@ -1,26 +1,31 @@
import type { import {
DatabaseIntrospector, type DatabaseIntrospector,
DatabaseMetadata, type DatabaseMetadata,
DatabaseMetadataOptions, type DatabaseMetadataOptions,
ExpressionBuilder, type Kysely,
Kysely, ParseJSONResultsPlugin,
SchemaMetadata, type SchemaMetadata,
TableMetadata, type TableMetadata,
type KyselyPlugin,
} from "kysely"; } from "kysely";
import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely"; import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely";
import type { ConnectionIntrospector, IndexMetadata } from "./Connection"; import type { ConnectionIntrospector, IndexMetadata } from "./Connection";
import { KyselyPluginRunner } from "data";
export type SqliteIntrospectorConfig = { export type SqliteIntrospectorConfig = {
excludeTables?: string[]; excludeTables?: string[];
plugins?: KyselyPlugin[];
}; };
export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntrospector { export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntrospector {
readonly #db: Kysely<any>; readonly #db: Kysely<any>;
readonly _excludeTables: string[] = []; readonly _excludeTables: string[] = [];
readonly _plugins: KyselyPlugin[];
constructor(db: Kysely<any>, config: SqliteIntrospectorConfig = {}) { constructor(db: Kysely<any>, config: SqliteIntrospectorConfig = {}) {
this.#db = db; this.#db = db;
this._excludeTables = config.excludeTables ?? []; this._excludeTables = config.excludeTables ?? [];
this._plugins = config.plugins ?? [new ParseJSONResultsPlugin()];
} }
async getSchemas(): Promise<SchemaMetadata[]> { async getSchemas(): Promise<SchemaMetadata[]> {
@@ -28,86 +33,96 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
return []; return [];
} }
async getIndices(tbl_name?: string): Promise<IndexMetadata[]> { async getSchema() {
const indices = await this.#db const excluded = [
.selectFrom("sqlite_master") ...this._excludeTables,
.where("type", "=", "index") DEFAULT_MIGRATION_TABLE,
.$if(!!tbl_name, (eb) => eb.where("tbl_name", "=", tbl_name)) DEFAULT_MIGRATION_LOCK_TABLE,
.select("name") ];
.$castTo<{ name: string }>() const query = sql`
.execute(); SELECT m.name, m.type, m.sql,
(SELECT json_group_array(
json_object(
'name', p.name,
'type', p.type,
'notnull', p."notnull",
'default', p.dflt_value,
'primary_key', p.pk
)) FROM pragma_table_info(m.name) p) AS columns,
(SELECT json_group_array(
json_object(
'name', i.name,
'origin', i.origin,
'partial', i.partial,
'sql', im.sql,
'columns', (SELECT json_group_array(
json_object(
'name', ii.name,
'seqno', ii.seqno
)) FROM pragma_index_info(i.name) ii)
)) FROM pragma_index_list(m.name) i
LEFT JOIN sqlite_master im ON im.name = i.name
AND im.type = 'index'
) AS indices
FROM sqlite_master m
WHERE m.type IN ('table', 'view')
and m.name not like 'sqlite_%'
and m.name not in (${excluded.join(", ")})
`;
return Promise.all(indices.map(({ name }) => this.#getIndexMetadata(name))); const result = await query.execute(this.#db);
} const runner = new KyselyPluginRunner(this._plugins ?? []);
const tables = (await runner.transformResultRows(result.rows)) as unknown as {
name: string;
type: string;
sql: string;
columns: {
name: string;
type: string;
notnull: number;
dflt_value: any;
pk: number;
}[];
indices: {
name: string;
origin: string;
partial: number;
sql: string;
columns: { name: string; seqno: number }[];
}[];
}[];
async #getIndexMetadata(index: string): Promise<IndexMetadata> { //console.log("tables", tables);
const db = this.#db; return tables.map((table) => ({
name: table.name,
isView: table.type === "view",
columns: table.columns.map((col) => {
const autoIncrementCol = table.sql
?.split(/[\(\),]/)
?.find((it) => it.toLowerCase().includes("autoincrement"))
?.trimStart()
?.split(/\s+/)?.[0]
?.replace(/["`]/g, "");
// Get the SQL that was used to create the index. return {
const indexDefinition = await db name: col.name,
.selectFrom("sqlite_master") dataType: col.type,
.where("name", "=", index) isNullable: !col.notnull,
.select(["sql", "tbl_name", "type"]) isAutoIncrementing: col.name === autoIncrementCol,
.$castTo<{ sql: string | undefined; tbl_name: string; type: string }>() hasDefaultValue: col.dflt_value != null,
.executeTakeFirstOrThrow(); comment: undefined,
};
//console.log("--indexDefinition--", indexDefinition, index); }),
indices: table.indices.map((index) => ({
// check unique by looking for the word "unique" in the sql name: index.name,
const isUnique = indexDefinition.sql?.match(/unique/i) != null; table: table.name,
isUnique: index.sql?.match(/unique/i) != null,
const columns = await db columns: index.columns.map((col) => ({
.selectFrom( name: col.name,
sql<{ order: col.seqno,
seqno: number; })),
cid: number;
name: string;
}>`pragma_index_info(${index})`.as("index_info"),
)
.select(["seqno", "cid", "name"])
.orderBy("cid")
.execute();
return {
name: index,
table: indexDefinition.tbl_name,
isUnique: isUnique,
columns: columns.map((col) => ({
name: col.name,
order: col.seqno,
})), })),
}; }));
}
private excludeTables(tables: string[] = []) {
return (eb: ExpressionBuilder<any, any>) => {
const and = tables.map((t) => eb("name", "!=", t));
return eb.and(and);
};
}
async getTables(
options: DatabaseMetadataOptions = { withInternalKyselyTables: false },
): Promise<TableMetadata[]> {
let query = this.#db
.selectFrom("sqlite_master")
.where("type", "in", ["table", "view"])
.where("name", "not like", "sqlite_%")
.select("name")
.orderBy("name")
.$castTo<{ name: string }>();
if (!options.withInternalKyselyTables) {
query = query.where(
this.excludeTables([DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK_TABLE]),
);
}
if (this._excludeTables.length > 0) {
query = query.where(this.excludeTables(this._excludeTables));
}
const tables = await query.execute();
return Promise.all(tables.map(({ name }) => this.#getTableMetadata(name)));
} }
async getMetadata(options?: DatabaseMetadataOptions): Promise<DatabaseMetadata> { async getMetadata(options?: DatabaseMetadataOptions): Promise<DatabaseMetadata> {
@@ -116,49 +131,21 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
}; };
} }
async #getTableMetadata(table: string): Promise<TableMetadata> { async getIndices(tbl_name?: string): Promise<IndexMetadata[]> {
const db = this.#db; const schema = await this.getSchema();
return schema
.flatMap((table) => table.indices)
.filter((index) => !tbl_name || index.table === tbl_name);
}
// Get the SQL that was used to create the table. async getTables(
const tableDefinition = await db options: DatabaseMetadataOptions = { withInternalKyselyTables: false },
.selectFrom("sqlite_master") ): Promise<TableMetadata[]> {
.where("name", "=", table) const schema = await this.getSchema();
.select(["sql", "type"]) return schema.map((table) => ({
.$castTo<{ sql: string | undefined; type: string }>() name: table.name,
.executeTakeFirstOrThrow(); isView: table.isView,
columns: table.columns,
// Try to find the name of the column that has `autoincrement` 🤦 }));
const autoIncrementCol = tableDefinition.sql
?.split(/[\(\),]/)
?.find((it) => it.toLowerCase().includes("autoincrement"))
?.trimStart()
?.split(/\s+/)?.[0]
?.replace(/["`]/g, "");
const columns = await db
.selectFrom(
sql<{
name: string;
type: string;
notnull: 0 | 1;
dflt_value: any;
}>`pragma_table_info(${table})`.as("table_info"),
)
.select(["name", "type", "notnull", "dflt_value"])
.orderBy("cid")
.execute();
return {
name: table,
isView: tableDefinition.type === "view",
columns: columns.map((col) => ({
name: col.name,
dataType: col.type,
isNullable: !col.notnull,
isAutoIncrementing: col.name === autoIncrementCol,
hasDefaultValue: col.dflt_value != null,
comment: undefined,
})),
};
} }
} }

View File

@@ -3,25 +3,25 @@ import { Kysely, SqliteDialect } from "kysely";
import { SqliteConnection } from "./SqliteConnection"; import { SqliteConnection } from "./SqliteConnection";
import { SqliteIntrospector } from "./SqliteIntrospector"; import { SqliteIntrospector } from "./SqliteIntrospector";
const plugins = [new ParseJSONResultsPlugin()];
class CustomSqliteDialect extends SqliteDialect { class CustomSqliteDialect extends SqliteDialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector { override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new SqliteIntrospector(db, { return new SqliteIntrospector(db, {
excludeTables: ["test_table"], excludeTables: ["test_table"],
plugins,
}); });
} }
} }
export class SqliteLocalConnection extends SqliteConnection { export class SqliteLocalConnection extends SqliteConnection {
constructor(private database: SqliteDatabase) { constructor(private database: SqliteDatabase) {
const plugins = [new ParseJSONResultsPlugin()];
const kysely = new Kysely({ const kysely = new Kysely({
dialect: new CustomSqliteDialect({ database }), dialect: new CustomSqliteDialect({ database }),
plugins, plugins,
//log: ["query"],
}); });
super(kysely); super(kysely, {}, plugins);
this.plugins = plugins;
} }
override supportsIndices(): boolean { override supportsIndices(): boolean {

View File

@@ -0,0 +1,71 @@
import {
Kysely,
PostgresDialect,
type DatabaseIntrospector,
type ColumnDataType,
type ColumnDefinitionBuilder,
ParseJSONResultsPlugin,
} from "kysely";
import pg from "pg";
import { PostgresIntrospector } from "./PostgresIntrospector";
import { type FieldSpec, type SchemaResponse, Connection } from "data/connection/Connection";
export type PostgresConnectionConfig = pg.PoolConfig;
const plugins = [new ParseJSONResultsPlugin()];
class CustomPostgresDialect extends PostgresDialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new PostgresIntrospector(db);
}
}
export class PostgresConnection extends Connection {
constructor(config: PostgresConnectionConfig) {
const kysely = new Kysely({
dialect: new CustomPostgresDialect({
pool: new pg.Pool(config),
}),
plugins,
//log: ["query", "error"],
});
super(kysely, {}, plugins);
}
override supportsIndices(): boolean {
return true;
}
override getFieldSchema(spec: FieldSpec): SchemaResponse {
this.validateFieldSpecType(spec.type);
let type: ColumnDataType = spec.primary ? "serial" : spec.type;
switch (spec.type) {
case "date":
case "datetime":
type = "timestamp";
break;
case "text":
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();
},
];
}
}

View File

@@ -0,0 +1,185 @@
import {
type DatabaseIntrospector,
type DatabaseMetadata,
type DatabaseMetadataOptions,
type SchemaMetadata,
type TableMetadata,
type Kysely,
type KyselyPlugin,
ParseJSONResultsPlugin,
} from "kysely";
import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely";
import { KyselyPluginRunner } from "data";
import type { IndexMetadata } from "data/connection/Connection";
export type PostgresIntrospectorConfig = {
excludeTables?: string[];
plugins?: KyselyPlugin[];
};
export class PostgresIntrospector implements DatabaseIntrospector {
readonly #db: Kysely<any>;
readonly _excludeTables: string[] = [];
readonly _plugins: KyselyPlugin[];
constructor(db: Kysely<any>, config: PostgresIntrospectorConfig = {}) {
this.#db = db;
this._excludeTables = config.excludeTables ?? [];
this._plugins = config.plugins ?? [new ParseJSONResultsPlugin()];
}
async getSchemas(): Promise<SchemaMetadata[]> {
const rawSchemas = await this.#db
.selectFrom("pg_catalog.pg_namespace")
.select("nspname")
.$castTo<RawSchemaMetadata>()
.execute();
return rawSchemas.map((it) => ({ name: it.nspname }));
}
async getMetadata(options?: DatabaseMetadataOptions): Promise<DatabaseMetadata> {
return {
tables: await this.getTables(options),
};
}
async getSchema() {
const excluded = [
...this._excludeTables,
DEFAULT_MIGRATION_TABLE,
DEFAULT_MIGRATION_LOCK_TABLE,
];
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 (${excluded.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 result = await query.execute(this.#db);
const runner = new KyselyPluginRunner(this._plugins ?? []);
const tables = (await runner.transformResultRows(result.rows)) as unknown as {
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 }[];
}[];
}[];
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,
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,
})),
})),
}));
}
async getIndices(tbl_name?: string): Promise<IndexMetadata[]> {
const schema = await this.getSchema();
return schema
.flatMap((table) => table.indices)
.filter((index) => !tbl_name || index.table === tbl_name);
}
async getTables(
options: DatabaseMetadataOptions = { withInternalKyselyTables: false },
): Promise<TableMetadata[]> {
const schema = await this.getSchema();
return schema.map((table) => ({
name: table.name,
isView: table.isView,
columns: table.columns,
}));
}
}
interface RawSchemaMetadata {
nspname: string;
}

View File

@@ -32,9 +32,11 @@ export class BooleanField<Required extends true | false = false> extends Field<
} }
} }
schema() { override schema() {
// @todo: potentially use "integer" instead return Object.freeze({
return this.useSchemaHelper("boolean"); ...super.schema()!,
type: "boolean",
});
} }
override getHtmlConfig() { override getHtmlConfig() {

View File

@@ -32,8 +32,10 @@ export class DateField<Required extends true | false = false> extends Field<
} }
override schema() { override schema() {
const type = this.config.type === "datetime" ? "datetime" : "date"; return Object.freeze({
return this.useSchemaHelper(type); ...super.schema()!,
type: this.config.type === "datetime" ? "datetime" : "date",
});
} }
override getHtmlConfig() { override getHtmlConfig() {

View File

@@ -66,10 +66,6 @@ export class EnumField<Required extends true | false = false, TypeOverride = str
return enumFieldConfigSchema; return enumFieldConfigSchema;
} }
override schema() {
return this.useSchemaHelper("text");
}
getOptions(): { label: string; value: string }[] { getOptions(): { label: string; value: string }[] {
const options = this.config?.options ?? { type: "strings", values: [] }; const options = this.config?.options ?? { type: "strings", values: [] };

View File

@@ -1,16 +1,16 @@
import { import {
parse,
snakeToPascalWithSpaces,
type Static, type Static,
StringEnum, StringEnum,
type TSchema, type TSchema,
Type, Type,
TypeInvalidError, TypeInvalidError,
parse,
snakeToPascalWithSpaces,
} from "core/utils"; } from "core/utils";
import type { ColumnBuilderCallback, ColumnDataType, ColumnDefinitionBuilder } from "kysely";
import type { HTMLInputTypeAttribute, InputHTMLAttributes } from "react"; import type { HTMLInputTypeAttribute, InputHTMLAttributes } from "react";
import type { EntityManager } from "../entities"; import type { EntityManager } from "../entities";
import { InvalidFieldConfigException, TransformPersistFailedException } from "../errors"; import { InvalidFieldConfigException, TransformPersistFailedException } from "../errors";
import type { FieldSpec } from "data/connection/Connection";
// @todo: contexts need to be reworked // @todo: contexts need to be reworked
// e.g. "table" is irrelevant, because if read is not given, it fails // e.g. "table" is irrelevant, because if read is not given, it fails
@@ -67,8 +67,6 @@ export const baseFieldConfigSchema = Type.Object(
); );
export type BaseFieldConfig = Static<typeof baseFieldConfigSchema>; export type BaseFieldConfig = Static<typeof baseFieldConfigSchema>;
export type SchemaResponse = [string, ColumnDataType, ColumnBuilderCallback] | undefined;
export abstract class Field< export abstract class Field<
Config extends BaseFieldConfig = BaseFieldConfig, Config extends BaseFieldConfig = BaseFieldConfig,
Type = any, Type = any,
@@ -106,25 +104,18 @@ export abstract class Field<
protected abstract getSchema(): TSchema; protected abstract getSchema(): TSchema;
protected useSchemaHelper(
type: ColumnDataType,
builder?: (col: ColumnDefinitionBuilder) => ColumnDefinitionBuilder,
): SchemaResponse {
return [
this.name,
type,
(col: ColumnDefinitionBuilder) => {
if (builder) return builder(col);
return col;
},
];
}
/** /**
* Used in SchemaManager.ts * Used in SchemaManager.ts
* @param em * @param em
*/ */
abstract schema(em: EntityManager<any>): SchemaResponse; schema(): FieldSpec | undefined {
return Object.freeze({
name: this.name,
type: "text",
nullable: true,
dflt: this.getDefault(),
});
}
hasDefault() { hasDefault() {
return this.config.default_value !== undefined; return this.config.default_value !== undefined;

View File

@@ -18,10 +18,6 @@ export class JsonField<Required extends true | false = false, TypeOverride = obj
return jsonFieldConfigSchema; return jsonFieldConfigSchema;
} }
override schema() {
return this.useSchemaHelper("text");
}
/** /**
* Transform value after retrieving from database * Transform value after retrieving from database
* @param value * @param value

View File

@@ -36,10 +36,6 @@ export class JsonSchemaField<
return jsonSchemaFieldConfigSchema; return jsonSchemaFieldConfigSchema;
} }
override schema() {
return this.useSchemaHelper("text");
}
getJsonSchema(): JsonSchema { getJsonSchema(): JsonSchema {
return this.config?.schema as JsonSchema; return this.config?.schema as JsonSchema;
} }

View File

@@ -44,8 +44,11 @@ export class NumberField<Required extends true | false = false> extends Field<
}; };
} }
schema() { override schema() {
return this.useSchemaHelper("integer"); return Object.freeze({
...super.schema()!,
type: "integer",
});
} }
override getValue(value: any, context?: TRenderContext): any { override getValue(value: any, context?: TRenderContext): any {

View File

@@ -30,9 +30,12 @@ export class PrimaryField<Required extends true | false = false> extends Field<
return baseFieldConfigSchema; return baseFieldConfigSchema;
} }
schema() { override schema() {
return this.useSchemaHelper("integer", (col) => { return Object.freeze({
return col.primaryKey().notNull().autoIncrement(); type: "integer",
name: this.name,
primary: true,
nullable: false,
}); });
} }

View File

@@ -47,10 +47,6 @@ export class TextField<Required extends true | false = false> extends Field<
return textFieldConfigSchema; return textFieldConfigSchema;
} }
override schema() {
return this.useSchemaHelper("text");
}
override getHtmlConfig() { override getHtmlConfig() {
if (this.config.html_config) { if (this.config.html_config) {
return this.config.html_config as any; return this.config.html_config as any;

View File

@@ -17,7 +17,7 @@ export class VirtualField extends Field<VirtualFieldConfig> {
return virtualFieldConfigSchema; return virtualFieldConfigSchema;
} }
schema() { override schema() {
return undefined; return undefined;
} }

View File

@@ -1,6 +1,6 @@
import { type Static, StringEnum, Type } from "core/utils"; import { type Static, StringEnum, Type } from "core/utils";
import type { EntityManager } from "../entities"; import type { EntityManager } from "../entities";
import { Field, type SchemaResponse, baseFieldConfigSchema } from "../fields"; import { Field, baseFieldConfigSchema } from "../fields";
import type { EntityRelation } from "./EntityRelation"; import type { EntityRelation } from "./EntityRelation";
import type { EntityRelationAnchor } from "./EntityRelationAnchor"; import type { EntityRelationAnchor } from "./EntityRelationAnchor";
@@ -72,14 +72,12 @@ export class RelationField extends Field<RelationFieldConfig> {
return this.config.target_field!; return this.config.target_field!;
} }
override schema(): SchemaResponse { override schema() {
return this.useSchemaHelper("integer", (col) => { return Object.freeze({
//col.references('person.id').onDelete('cascade').notNull() ...super.schema()!,
// @todo: implement cascading? type: "integer",
references: `${this.config.target}.${this.config.target_field}`,
return col onDelete: this.config.on_delete ?? "set null",
.references(`${this.config.target}.${this.config.target_field}`)
.onDelete(this.config.on_delete ?? "set null");
}); });
} }

View File

@@ -1,7 +1,7 @@
import type { AlterTableColumnAlteringBuilder, CompiledQuery, TableMetadata } from "kysely"; import type { AlterTableColumnAlteringBuilder, CompiledQuery, TableMetadata } from "kysely";
import type { IndexMetadata } from "../connection/Connection"; import type { IndexMetadata, SchemaResponse } from "../connection/Connection";
import type { Entity, EntityManager } from "../entities"; import type { Entity, EntityManager } from "../entities";
import { PrimaryField, type SchemaResponse } from "../fields"; import { PrimaryField } from "../fields";
type IntrospectedTable = TableMetadata & { type IntrospectedTable = TableMetadata & {
indices: IndexMetadata[]; indices: IndexMetadata[];
@@ -239,10 +239,9 @@ export class SchemaManager {
for (const column of columns) { for (const column of columns) {
const field = this.em.entity(table).getField(column)!; const field = this.em.entity(table).getField(column)!;
const fieldSchema = field.schema(this.em); const fieldSchema = field.schema();
if (Array.isArray(fieldSchema) && fieldSchema.length === 3) { if (fieldSchema) {
schemas.push(fieldSchema); schemas.push(this.em.connection.getFieldSchema(fieldSchema));
//throw new Error(`Field "${field.name}" on entity "${table}" has no schema`);
} }
} }

View File

@@ -27,7 +27,7 @@
}, },
"app": { "app": {
"name": "bknd", "name": "bknd",
"version": "0.9.0-rc.1-11", "version": "0.9.0",
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"dependencies": { "dependencies": {
"@cfworker/json-schema": "^2.0.1", "@cfworker/json-schema": "^2.0.1",
@@ -71,6 +71,7 @@
"@rjsf/core": "5.22.2", "@rjsf/core": "5.22.2",
"@tabler/icons-react": "3.18.0", "@tabler/icons-react": "3.18.0",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/pg": "^8.11.11",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.3",
@@ -82,6 +83,7 @@
"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.4.47", "postcss": "^8.4.47",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
@@ -121,6 +123,7 @@
"version": "0.5.1", "version": "0.5.1",
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"bknd": "workspace:*",
"tsdx": "^0.14.1", "tsdx": "^0.14.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
}, },
@@ -1115,6 +1118,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/prop-types": ["@types/prop-types@15.7.13", "", {}, "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA=="], "@types/prop-types": ["@types/prop-types@15.7.13", "", {}, "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA=="],
@@ -2457,6 +2462,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.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="], "ohash": ["ohash@1.1.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="],
"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=="],
@@ -2527,6 +2534,24 @@
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
"pg": ["pg@8.13.3", "", { "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.7.1", "pg-protocol": "^1.7.1", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ=="],
"pg-cloudflare": ["pg-cloudflare@1.1.1", "", {}, "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="],
"pg-connection-string": ["pg-connection-string@2.7.0", "", {}, "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="],
"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.7.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw=="],
"pg-protocol": ["pg-protocol@1.7.1", "", {}, "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ=="],
"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=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
@@ -2577,6 +2602,16 @@
"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@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
"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=="],
"prettier": ["prettier@1.19.1", "", { "bin": { "prettier": "./bin-prettier.js" } }, "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew=="], "prettier": ["prettier@1.19.1", "", { "bin": { "prettier": "./bin-prettier.js" } }, "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew=="],
@@ -4145,6 +4180,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=="],
"postcss-load-config/lilconfig": ["lilconfig@3.1.2", "", {}, "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow=="], "postcss-load-config/lilconfig": ["lilconfig@3.1.2", "", {}, "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow=="],
@@ -4813,6 +4850,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=="],