diff --git a/app/__test__/App.spec.ts b/app/__test__/App.spec.ts index 395aec0..79fdc51 100644 --- a/app/__test__/App.spec.ts +++ b/app/__test__/App.spec.ts @@ -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 { getDummyConnection } from "./helper"; const { dummyConnection, afterAllCleanup } = getDummyConnection(); -afterAll(afterAllCleanup); +afterEach(afterAllCleanup); describe("App tests", async () => { test("boots and pongs", async () => { @@ -12,4 +12,16 @@ describe("App tests", async () => { //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()); + });*/ }); diff --git a/app/__test__/data/pg.spec.ts b/app/__test__/data/pg.spec.ts new file mode 100644 index 0000000..bf65c66 --- /dev/null +++ b/app/__test__/data/pg.spec.ts @@ -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(); + }); +}); diff --git a/app/__test__/data/relations.test.ts b/app/__test__/data/relations.test.ts index b4ff708..07ecd19 100644 --- a/app/__test__/data/relations.test.ts +++ b/app/__test__/data/relations.test.ts @@ -27,7 +27,7 @@ describe("Relations", async () => { const sql1 = schema .createTable("posts") - .addColumn(...r1.schema()!) + .addColumn(...em.connection.getFieldSchema(r1.schema())!) .compile().sql; expect(sql1).toBe( @@ -43,7 +43,7 @@ describe("Relations", async () => { const sql2 = schema .createTable("posts") - .addColumn(...r2.schema()!) + .addColumn(...em.connection.getFieldSchema(r2.schema())!) .compile().sql; expect(sql2).toBe( diff --git a/app/__test__/data/specs/SchemaManager.spec.ts b/app/__test__/data/specs/SchemaManager.spec.ts index 9e9fe90..7c2322b 100644 --- a/app/__test__/data/specs/SchemaManager.spec.ts +++ b/app/__test__/data/specs/SchemaManager.spec.ts @@ -15,7 +15,7 @@ describe("SchemaManager tests", async () => { const em = new EntityManager([entity], dummyConnection, [], [index]); const schema = new SchemaManager(em); - const introspection = schema.getIntrospectionFromEntity(em.entities[0]); + const introspection = schema.getIntrospectionFromEntity(em.entities[0]!); expect(introspection).toEqual({ name: "test", isView: false, @@ -109,7 +109,7 @@ describe("SchemaManager tests", async () => { await schema.sync({ force: true, drop: true }); const diffAfter = await schema.getDiff(); - console.log("diffAfter", diffAfter); + //console.log("diffAfter", diffAfter); expect(diffAfter.length).toBe(0); await kysely.schema.dropTable(table).execute(); diff --git a/app/__test__/data/specs/connection/SqliteIntrospector.spec.ts b/app/__test__/data/specs/connection/SqliteIntrospector.spec.ts new file mode 100644 index 0000000..84e4a29 --- /dev/null +++ b/app/__test__/data/specs/connection/SqliteIntrospector.spec.ts @@ -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, + }, + ], + }, + ]); + }); +}); diff --git a/app/__test__/data/specs/fields/Field.spec.ts b/app/__test__/data/specs/fields/Field.spec.ts index 45e4351..82ba9de 100644 --- a/app/__test__/data/specs/fields/Field.spec.ts +++ b/app/__test__/data/specs/fields/Field.spec.ts @@ -1,23 +1,29 @@ import { describe, expect, test } from "bun:test"; -import { Default, parse, stripMark } from "../../../../src/core/utils"; -import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data"; -import { runBaseFieldTests, transformPersist } from "./inc"; +import { Default, stripMark } from "../../../../src/core/utils"; +import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field"; +import { runBaseFieldTests } from "./inc"; describe("[data] Field", async () => { class FieldSpec extends Field { - schema(): SchemaResponse { - return this.useSchemaHelper("text"); - } getSchema() { 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" }); test("default config", async () => { 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 () => { diff --git a/app/__test__/data/specs/fields/PrimaryField.spec.ts b/app/__test__/data/specs/fields/PrimaryField.spec.ts index 5d6dd54..6be0166 100644 --- a/app/__test__/data/specs/fields/PrimaryField.spec.ts +++ b/app/__test__/data/specs/fields/PrimaryField.spec.ts @@ -10,7 +10,12 @@ describe("[data] PrimaryField", async () => { test("schema", () => { 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 () => { diff --git a/app/__test__/data/specs/fields/inc.ts b/app/__test__/data/specs/fields/inc.ts index 1754c20..ff2d00e 100644 --- a/app/__test__/data/specs/fields/inc.ts +++ b/app/__test__/data/specs/fields/inc.ts @@ -34,11 +34,14 @@ export function runBaseFieldTests( test("schema", () => { expect(noConfigField.name).toBe("no_config"); - expect(noConfigField.schema(null as any)).toEqual([ - "no_config", - config.schemaType, - expect.any(Function), - ]); + + const { type, name, nullable, dflt } = noConfigField.schema()!; + expect({ type, name, nullable, dflt }).toEqual({ + type: config.schemaType as any, + name: "no_config", + nullable: true, // always true + dflt: undefined, // never using default value + }); }); test("hasDefault", async () => { diff --git a/app/__test__/integration/auth.integration.test.ts b/app/__test__/integration/auth.integration.test.ts index 9b2bb51..298ad31 100644 --- a/app/__test__/integration/auth.integration.test.ts +++ b/app/__test__/integration/auth.integration.test.ts @@ -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 type { AuthResponse } from "../../src/auth"; import { auth } from "../../src/auth/middlewares"; 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); afterAll(enableConsoleLog); @@ -64,6 +67,7 @@ const configs = { function createAuthApp() { const app = createApp({ + connection: dummyConnection, initialConfig: { auth: configs.auth, }, diff --git a/app/package.json b/app/package.json index b3a7974..14ff0a0 100644 --- a/app/package.json +++ b/app/package.json @@ -63,16 +63,17 @@ "@aws-sdk/client-s3": "^3.613.0", "@bluwy/giget-core": "^0.1.2", "@dagrejs/dagre": "^1.1.4", - "@mantine/modals": "^7.13.4", - "@mantine/notifications": "^7.13.4", "@hono/typebox-validator": "^0.2.6", "@hono/vite-dev-server": "^0.17.0", "@hono/zod-validator": "^0.4.1", "@hookform/resolvers": "^3.9.1", "@libsql/kysely-libsql": "^0.4.1", + "@mantine/modals": "^7.13.4", + "@mantine/notifications": "^7.13.4", "@rjsf/core": "5.22.2", "@tabler/icons-react": "3.18.0", "@types/node": "^22.10.0", + "@types/pg": "^8.11.11", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", @@ -84,6 +85,7 @@ "kysely-d1": "^0.3.0", "open": "^10.1.0", "openapi-types": "^12.1.3", + "pg": "^8.13.3", "postcss": "^8.4.47", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", diff --git a/app/src/data/connection/Connection.ts b/app/src/data/connection/Connection.ts index 2a3933b..c80679b 100644 --- a/app/src/data/connection/Connection.ts +++ b/app/src/data/connection/Connection.ts @@ -1,9 +1,12 @@ import { type AliasableExpression, + type ColumnBuilderCallback, + type ColumnDataType, type DatabaseIntrospector, type Expression, type Kysely, type KyselyPlugin, + type OnModifyForeignAction, type RawBuilder, type SelectQueryBuilder, type SelectQueryNode, @@ -29,6 +32,38 @@ export interface SelectQueryBuilderExpression extends AliasableExpression 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 = { jsonObjectFrom(expr: SelectQueryBuilderExpression): RawBuilder | null>; jsonArrayFrom(expr: SelectQueryBuilderExpression): RawBuilder[]>; @@ -108,4 +143,15 @@ export abstract class Connection { 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; } diff --git a/app/src/data/connection/LibsqlConnection.ts b/app/src/data/connection/LibsqlConnection.ts index d341adc..a766931 100644 --- a/app/src/data/connection/LibsqlConnection.ts +++ b/app/src/data/connection/LibsqlConnection.ts @@ -12,10 +12,13 @@ export type LibSqlCredentials = Config & { protocol?: (typeof LIBSQL_PROTOCOLS)[number]; }; +const plugins = [new FilterNumericKeysPlugin(), new ParseJSONResultsPlugin()]; + class CustomLibsqlDialect extends LibsqlDialect { override createIntrospector(db: Kysely): DatabaseIntrospector { return new SqliteIntrospector(db, { excludeTables: ["libsql_wasm_func_table"], + plugins, }); } } @@ -26,7 +29,6 @@ export class LibsqlConnection extends SqliteConnection { constructor(client: Client); constructor(credentials: LibSqlCredentials); constructor(clientOrCredentials: Client | LibSqlCredentials) { - const plugins = [new FilterNumericKeysPlugin(), new ParseJSONResultsPlugin()]; let client: Client; if (clientOrCredentials && "url" in clientOrCredentials) { let { url, authToken, protocol } = clientOrCredentials; diff --git a/app/src/data/connection/SqliteConnection.ts b/app/src/data/connection/SqliteConnection.ts index 2572667..665eb44 100644 --- a/app/src/data/connection/SqliteConnection.ts +++ b/app/src/data/connection/SqliteConnection.ts @@ -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 { Connection, type DbFunctions } from "./Connection"; +import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } from "./Connection"; export class SqliteConnection extends Connection { constructor(kysely: Kysely, fn: Partial = {}, plugins: KyselyPlugin[] = []) { @@ -19,4 +19,32 @@ export class SqliteConnection extends Connection { override supportsIndices(): boolean { 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; + } } diff --git a/app/src/data/connection/SqliteIntrospector.ts b/app/src/data/connection/SqliteIntrospector.ts index cf68816..985e1cd 100644 --- a/app/src/data/connection/SqliteIntrospector.ts +++ b/app/src/data/connection/SqliteIntrospector.ts @@ -1,26 +1,31 @@ -import type { - DatabaseIntrospector, - DatabaseMetadata, - DatabaseMetadataOptions, - ExpressionBuilder, - Kysely, - SchemaMetadata, - TableMetadata, +import { + type DatabaseIntrospector, + type DatabaseMetadata, + type DatabaseMetadataOptions, + type Kysely, + ParseJSONResultsPlugin, + type SchemaMetadata, + type TableMetadata, + type KyselyPlugin, } from "kysely"; import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely"; import type { ConnectionIntrospector, IndexMetadata } from "./Connection"; +import { KyselyPluginRunner } from "data"; export type SqliteIntrospectorConfig = { excludeTables?: string[]; + plugins?: KyselyPlugin[]; }; export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntrospector { readonly #db: Kysely; readonly _excludeTables: string[] = []; + readonly _plugins: KyselyPlugin[]; constructor(db: Kysely, config: SqliteIntrospectorConfig = {}) { this.#db = db; this._excludeTables = config.excludeTables ?? []; + this._plugins = config.plugins ?? [new ParseJSONResultsPlugin()]; } async getSchemas(): Promise { @@ -28,86 +33,96 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro return []; } - async getIndices(tbl_name?: string): Promise { - const indices = await this.#db - .selectFrom("sqlite_master") - .where("type", "=", "index") - .$if(!!tbl_name, (eb) => eb.where("tbl_name", "=", tbl_name)) - .select("name") - .$castTo<{ name: string }>() - .execute(); + async getSchema() { + const excluded = [ + ...this._excludeTables, + DEFAULT_MIGRATION_TABLE, + DEFAULT_MIGRATION_LOCK_TABLE, + ]; + const query = sql` + 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 { - const db = this.#db; + //console.log("tables", tables); + 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. - const indexDefinition = await db - .selectFrom("sqlite_master") - .where("name", "=", index) - .select(["sql", "tbl_name", "type"]) - .$castTo<{ sql: string | undefined; tbl_name: string; type: string }>() - .executeTakeFirstOrThrow(); - - //console.log("--indexDefinition--", indexDefinition, index); - - // check unique by looking for the word "unique" in the sql - const isUnique = indexDefinition.sql?.match(/unique/i) != null; - - const columns = await db - .selectFrom( - sql<{ - 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, + return { + name: col.name, + dataType: col.type, + isNullable: !col.notnull, + isAutoIncrementing: col.name === autoIncrementCol, + hasDefaultValue: col.dflt_value != 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, + })), })), - }; - } - - private excludeTables(tables: string[] = []) { - return (eb: ExpressionBuilder) => { - const and = tables.map((t) => eb("name", "!=", t)); - return eb.and(and); - }; - } - - async getTables( - options: DatabaseMetadataOptions = { withInternalKyselyTables: false }, - ): Promise { - 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 { @@ -116,49 +131,21 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro }; } - async #getTableMetadata(table: string): Promise { - const db = this.#db; + async getIndices(tbl_name?: string): Promise { + 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. - const tableDefinition = await db - .selectFrom("sqlite_master") - .where("name", "=", table) - .select(["sql", "type"]) - .$castTo<{ sql: string | undefined; type: string }>() - .executeTakeFirstOrThrow(); - - // 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, - })), - }; + async getTables( + options: DatabaseMetadataOptions = { withInternalKyselyTables: false }, + ): Promise { + const schema = await this.getSchema(); + return schema.map((table) => ({ + name: table.name, + isView: table.isView, + columns: table.columns, + })); } } diff --git a/app/src/data/connection/SqliteLocalConnection.ts b/app/src/data/connection/SqliteLocalConnection.ts index 7c26428..029808a 100644 --- a/app/src/data/connection/SqliteLocalConnection.ts +++ b/app/src/data/connection/SqliteLocalConnection.ts @@ -3,25 +3,25 @@ import { Kysely, SqliteDialect } from "kysely"; import { SqliteConnection } from "./SqliteConnection"; import { SqliteIntrospector } from "./SqliteIntrospector"; +const plugins = [new ParseJSONResultsPlugin()]; + class CustomSqliteDialect extends SqliteDialect { override createIntrospector(db: Kysely): DatabaseIntrospector { return new SqliteIntrospector(db, { excludeTables: ["test_table"], + plugins, }); } } export class SqliteLocalConnection extends SqliteConnection { constructor(private database: SqliteDatabase) { - const plugins = [new ParseJSONResultsPlugin()]; const kysely = new Kysely({ dialect: new CustomSqliteDialect({ database }), plugins, - //log: ["query"], }); - super(kysely); - this.plugins = plugins; + super(kysely, {}, plugins); } override supportsIndices(): boolean { diff --git a/app/src/data/connection/postgres/PostgresConnection.ts b/app/src/data/connection/postgres/PostgresConnection.ts new file mode 100644 index 0000000..a7d29e0 --- /dev/null +++ b/app/src/data/connection/postgres/PostgresConnection.ts @@ -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): 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(); + }, + ]; + } +} diff --git a/app/src/data/connection/postgres/PostgresIntrospector.ts b/app/src/data/connection/postgres/PostgresIntrospector.ts new file mode 100644 index 0000000..d41fc7c --- /dev/null +++ b/app/src/data/connection/postgres/PostgresIntrospector.ts @@ -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; + readonly _excludeTables: string[] = []; + readonly _plugins: KyselyPlugin[]; + + constructor(db: Kysely, config: PostgresIntrospectorConfig = {}) { + this.#db = db; + this._excludeTables = config.excludeTables ?? []; + this._plugins = config.plugins ?? [new ParseJSONResultsPlugin()]; + } + + async getSchemas(): Promise { + const rawSchemas = await this.#db + .selectFrom("pg_catalog.pg_namespace") + .select("nspname") + .$castTo() + .execute(); + + return rawSchemas.map((it) => ({ name: it.nspname })); + } + + async getMetadata(options?: DatabaseMetadataOptions): Promise { + 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 { + 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 { + const schema = await this.getSchema(); + return schema.map((table) => ({ + name: table.name, + isView: table.isView, + columns: table.columns, + })); + } +} + +interface RawSchemaMetadata { + nspname: string; +} diff --git a/app/src/data/fields/BooleanField.ts b/app/src/data/fields/BooleanField.ts index 6f73e6f..19d2978 100644 --- a/app/src/data/fields/BooleanField.ts +++ b/app/src/data/fields/BooleanField.ts @@ -32,9 +32,11 @@ export class BooleanField extends Field< } } - schema() { - // @todo: potentially use "integer" instead - return this.useSchemaHelper("boolean"); + override schema() { + return Object.freeze({ + ...super.schema()!, + type: "boolean", + }); } override getHtmlConfig() { diff --git a/app/src/data/fields/DateField.ts b/app/src/data/fields/DateField.ts index 5020376..c7ba901 100644 --- a/app/src/data/fields/DateField.ts +++ b/app/src/data/fields/DateField.ts @@ -32,8 +32,10 @@ export class DateField extends Field< } override schema() { - const type = this.config.type === "datetime" ? "datetime" : "date"; - return this.useSchemaHelper(type); + return Object.freeze({ + ...super.schema()!, + type: this.config.type === "datetime" ? "datetime" : "date", + }); } override getHtmlConfig() { diff --git a/app/src/data/fields/EnumField.ts b/app/src/data/fields/EnumField.ts index 79eee93..e8e8772 100644 --- a/app/src/data/fields/EnumField.ts +++ b/app/src/data/fields/EnumField.ts @@ -66,10 +66,6 @@ export class EnumField; -export type SchemaResponse = [string, ColumnDataType, ColumnBuilderCallback] | undefined; - export abstract class Field< Config extends BaseFieldConfig = BaseFieldConfig, Type = any, @@ -106,25 +104,18 @@ export abstract class Field< 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 * @param em */ - abstract schema(em: EntityManager): SchemaResponse; + schema(): FieldSpec | undefined { + return Object.freeze({ + name: this.name, + type: "text", + nullable: true, + dflt: this.getDefault(), + }); + } hasDefault() { return this.config.default_value !== undefined; diff --git a/app/src/data/fields/JsonField.ts b/app/src/data/fields/JsonField.ts index 62a7677..b25df60 100644 --- a/app/src/data/fields/JsonField.ts +++ b/app/src/data/fields/JsonField.ts @@ -18,10 +18,6 @@ export class JsonField extends Field< }; } - schema() { - return this.useSchemaHelper("integer"); + override schema() { + return Object.freeze({ + ...super.schema()!, + type: "integer", + }); } override getValue(value: any, context?: TRenderContext): any { diff --git a/app/src/data/fields/PrimaryField.ts b/app/src/data/fields/PrimaryField.ts index 6245944..dd3463f 100644 --- a/app/src/data/fields/PrimaryField.ts +++ b/app/src/data/fields/PrimaryField.ts @@ -30,9 +30,12 @@ export class PrimaryField extends Field< return baseFieldConfigSchema; } - schema() { - return this.useSchemaHelper("integer", (col) => { - return col.primaryKey().notNull().autoIncrement(); + override schema() { + return Object.freeze({ + type: "integer", + name: this.name, + primary: true, + nullable: false, }); } diff --git a/app/src/data/fields/TextField.ts b/app/src/data/fields/TextField.ts index 7ebcea5..8c318ec 100644 --- a/app/src/data/fields/TextField.ts +++ b/app/src/data/fields/TextField.ts @@ -47,10 +47,6 @@ export class TextField extends Field< return textFieldConfigSchema; } - override schema() { - return this.useSchemaHelper("text"); - } - override getHtmlConfig() { if (this.config.html_config) { return this.config.html_config as any; diff --git a/app/src/data/fields/VirtualField.ts b/app/src/data/fields/VirtualField.ts index d02869b..c03db19 100644 --- a/app/src/data/fields/VirtualField.ts +++ b/app/src/data/fields/VirtualField.ts @@ -17,7 +17,7 @@ export class VirtualField extends Field { return virtualFieldConfigSchema; } - schema() { + override schema() { return undefined; } diff --git a/app/src/data/relations/RelationField.ts b/app/src/data/relations/RelationField.ts index b38cc1c..6868797 100644 --- a/app/src/data/relations/RelationField.ts +++ b/app/src/data/relations/RelationField.ts @@ -1,6 +1,6 @@ import { type Static, StringEnum, Type } from "core/utils"; import type { EntityManager } from "../entities"; -import { Field, type SchemaResponse, baseFieldConfigSchema } from "../fields"; +import { Field, baseFieldConfigSchema } from "../fields"; import type { EntityRelation } from "./EntityRelation"; import type { EntityRelationAnchor } from "./EntityRelationAnchor"; @@ -72,14 +72,12 @@ export class RelationField extends Field { return this.config.target_field!; } - override schema(): SchemaResponse { - return this.useSchemaHelper("integer", (col) => { - //col.references('person.id').onDelete('cascade').notNull() - // @todo: implement cascading? - - return col - .references(`${this.config.target}.${this.config.target_field}`) - .onDelete(this.config.on_delete ?? "set null"); + override schema() { + return Object.freeze({ + ...super.schema()!, + type: "integer", + references: `${this.config.target}.${this.config.target_field}`, + onDelete: this.config.on_delete ?? "set null", }); } diff --git a/app/src/data/schema/SchemaManager.ts b/app/src/data/schema/SchemaManager.ts index 58a3127..c9bebc1 100644 --- a/app/src/data/schema/SchemaManager.ts +++ b/app/src/data/schema/SchemaManager.ts @@ -1,7 +1,7 @@ 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 { PrimaryField, type SchemaResponse } from "../fields"; +import { PrimaryField } from "../fields"; type IntrospectedTable = TableMetadata & { indices: IndexMetadata[]; @@ -239,10 +239,9 @@ export class SchemaManager { for (const column of columns) { const field = this.em.entity(table).getField(column)!; - const fieldSchema = field.schema(this.em); - if (Array.isArray(fieldSchema) && fieldSchema.length === 3) { - schemas.push(fieldSchema); - //throw new Error(`Field "${field.name}" on entity "${table}" has no schema`); + const fieldSchema = field.schema(); + if (fieldSchema) { + schemas.push(this.em.connection.getFieldSchema(fieldSchema)); } } diff --git a/bun.lock b/bun.lock index 43926d3..375ea66 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ }, "app": { "name": "bknd", - "version": "0.9.0-rc.1-11", + "version": "0.9.0", "bin": "./dist/cli/index.js", "dependencies": { "@cfworker/json-schema": "^2.0.1", @@ -71,6 +71,7 @@ "@rjsf/core": "5.22.2", "@tabler/icons-react": "3.18.0", "@types/node": "^22.10.0", + "@types/pg": "^8.11.11", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", @@ -82,6 +83,7 @@ "kysely-d1": "^0.3.0", "open": "^10.1.0", "openapi-types": "^12.1.3", + "pg": "^8.13.3", "postcss": "^8.4.47", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", @@ -121,6 +123,7 @@ "version": "0.5.1", "devDependencies": { "@types/bun": "latest", + "bknd": "workspace:*", "tsdx": "^0.14.1", "typescript": "^5.0.0", }, @@ -1115,6 +1118,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/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=="], + "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], + "ohash": ["ohash@1.1.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="], "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=="], + "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=="], "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=="], + "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=="], "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=="], + "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=="], "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=="], + "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=="],