import type { PonderTypeError } from "@/types/utils.js"; import { type BuildColumns, type ColumnBuilderBase, Table, type Writable, getTableName, } from "drizzle-orm"; import { toSnakeCase } from "drizzle-orm/casing"; import { type AnyPgColumn, type PrimaryKeyBuilder as DrizzlePrimaryKeyBuilder, type ExtraConfigColumn, ManualViewBuilder, type PgColumnBuilder, type PgColumnBuilderBase, PgEnumColumnBuilder, type PgEnumColumnBuilderInitial, PgTable, type PgTableExtraConfig, type PgTableWithColumns, type PgTextConfig, type TableConfig, ViewBuilder, primaryKey as drizzlePrimaryKey, } from "drizzle-orm/pg-core"; import { type PgColumnsBuilders as _PgColumnsBuilders, getPgColumnBuilders, } from "drizzle-orm/pg-core/columns/all"; import { PgBigintBuilder, type PgBigintBuilderInitial } from "./bigint.js"; import { PgBytesBuilder, type PgBytesBuilderInitial } from "./bytes.js"; import { PgHexBuilder, type PgHexBuilderInitial } from "./hex.js"; import { PgJsonBuilder, type PgJsonBuilderInitial, PgJsonbBuilder, type PgJsonbBuilderInitial, } from "./json.js"; import { PgTextBuilder, type PgTextBuilderInitial } from "./text.js"; // 16 digits for chain ID. export const MAX_DATABASE_OBJECT_NAME_LENGTH = 45; // Note: All of these database object names should be less than 63 characters, otherwise they will // be truncated by postgres. export const getLiveQueryTriggerName = () => { return "live_query"; }; export const getLiveQueryProcedureName = () => { return "live_query()"; }; export const getLiveQueryChannelName = (schema: string) => { return `${schema}_live_query`; }; export const getLiveQueryNotifyTriggerName = () => { return "live_query_notify"; }; /** * Returns the name of the trigger used to notify live queries for the views pattern. * @dev The trigger is placed in the base schema, but used to notify in the views schema. */ export const getViewsLiveQueryNotifyTriggerName = (viewsSchema: string) => { return `${viewsSchema}_live_query_notify`; }; export const getLiveQueryNotifyProcedureName = () => { return "live_query_notify()"; }; export const getLiveQueryTempTableName = () => { return "live_query_tables"; }; export const getPartitionName = (table: string | PgTable, chainId: number) => { return `${typeof table === "string" ? table : getTableName(table)}_${chainId}`; }; export const getReorgTableName = (table: string | PgTable) => { return `_reorg__${typeof table === "string" ? table : getTableName(table)}`; }; export const getReorgTriggerName = () => { return "reorg"; }; export const getReorgProcedureName = (table: string | PgTable) => { return `operation_reorg__${typeof table === "string" ? table : getTableName(table)}()`; }; export const getReorgSequenceName = () => { return "operation_id"; }; /** @internal */ function getColumnNameAndConfig< TConfig extends Record | undefined, >(a: string | TConfig | undefined, b: TConfig | undefined) { return { name: typeof a === "string" && a.length > 0 ? a : ("" as string), config: typeof a === "object" ? a : (b as TConfig), }; } // @ts-ignore export function hex(): PgHexBuilderInitial<"">; export function hex( columnName: name, ): PgHexBuilderInitial; export function hex(columnName?: string) { return new PgHexBuilder(columnName ?? ""); } // @ts-ignore export function bigint(): PgBigintBuilderInitial<"">; export function bigint( columnName: name, ): PgBigintBuilderInitial; export function bigint(columnName?: string) { return new PgBigintBuilder(columnName ?? ""); } export function json(): PgJsonBuilderInitial<"">; export function json( name: name, ): PgJsonBuilderInitial; export function json(name?: string) { return new PgJsonBuilder(name ?? ""); } export function jsonb(): PgJsonbBuilderInitial<"">; export function jsonb( name: name, ): PgJsonbBuilderInitial; export function jsonb(name?: string) { return new PgJsonbBuilder(name ?? ""); } // @ts-ignore export function bytes(): PgBytesBuilderInitial<"">; export function bytes( columnName: name, ): PgBytesBuilderInitial; export function bytes(columnName?: string) { return new PgBytesBuilder(columnName ?? ""); } export function text(): PgTextBuilderInitial<"", [string, ...string[]]>; export function text>( config?: PgTextConfig>, ): PgTextBuilderInitial<"", Writable>; export function text< TName extends string, U extends string, T extends Readonly<[U, ...U[]]>, >( name: TName, config?: PgTextConfig>, ): PgTextBuilderInitial>; export function text(a?: string | PgTextConfig, b: PgTextConfig = {}): any { const { name, config } = getColumnNameAndConfig(a, b); return new PgTextBuilder(name, config as any); } export const onchain = Symbol.for("ponder:onchain"); export type PrimaryKeyBuilder = DrizzlePrimaryKeyBuilder & { columnNames: columnNames }; export const primaryKey = < tableName extends string, column extends AnyPgColumn<{ tableName: tableName }> & { " name": string }, columns extends (AnyPgColumn<{ tableName: tableName }> & { " name": string; })[], >({ name, columns, }: { name?: string; columns: [column, ...columns] }) => drizzlePrimaryKey({ name, columns }) as PrimaryKeyBuilder< column[" name"] | columns[number][" name"] >; export type OnchainTable< T extends TableConfig & { extra: PgTableExtraConfig | undefined; } = TableConfig & { extra: PgTableExtraConfig | undefined }, > = PgTable & { [Key in keyof T["columns"]]: T["columns"][Key]; } & { [onchain]: true } & { enableRLS: () => Omit, "enableRLS">; }; export type BuildExtraConfigColumns< columns extends Record, > = { [key in keyof columns]: ExtraConfigColumn & { " name": key; }; }; export type PgColumnsBuilders = Omit< _PgColumnsBuilders, "bigint" | "serial" | "smallserial" | "bigserial" | "json" | "jsonb" > & { /** * Create an 8 byte number column. */ int8: _PgColumnsBuilders["bigint"]; /** * Create a column for hex strings. * * - Docs: https://ponder.sh/docs/api-reference/ponder/schema#onchaintable * * @example * import { hex, onchainTable } from "ponder"; * * export const account = onchainTable("account", (p) => ({ * address: p.hex(), * })); */ hex: typeof hex; /** * Create a column for Ethereum integers * * - Docs: https://ponder.sh/docs/api-reference/ponder/schema#onchaintable * * @example * import { bigint, onchainTable } from "ponder"; * * export const account = onchainTable("account", (p) => ({ * balance: p.bigint(), * })); */ bigint: typeof bigint; /** * Create a column for Ethereum bytes * * - Docs: https://ponder.sh/docs/api-reference/ponder/schema#onchaintable * * @example * import { bytes, onchainTable } from "ponder"; * * export const account = onchainTable("account", (p) => ({ * calldata: p.bytes(), * })); */ bytes: typeof bytes; json: typeof json; jsonb: typeof jsonb; }; /** * Create an onchain table. * * - Docs: https://ponder.sh/docs/api-reference/ponder/schema#onchaintable * * @example * import { onchainTable } from "ponder"; * * export const account = onchainTable("account", (p) => ({ * address: p.hex().primaryKey(), * balance: p.bigint().notNull(), * })); * * @param name - The table name in the database. * @param columns - The table columns. * @param extra - Config such as indexes or composite primary keys. * @returns The onchain table. */ export const onchainTable = < name extends string, columns extends Record, extra extends PgTableExtraConfig | undefined = undefined, >( name: name extends "" ? PonderTypeError<`Table name cannot be empty`> : name, columns: columns | ((columnTypes: PgColumnsBuilders) => columns), extraConfig?: (self: BuildExtraConfigColumns) => extra, ): OnchainTable<{ name: name; schema: undefined; columns: BuildColumns; extra: extra; dialect: "pg"; }> => { const schema = globalThis?.PONDER_NAMESPACE_BUILD?.schema; const table = pgTableWithSchema(name, columns, extraConfig as any, schema); // @ts-ignore table[onchain] = true; // @ts-ignore return table; }; /** * Create an onchain view. * * - Docs: https://ponder.sh/docs/api-reference/ponder/schema#onchainview * * @example * import { onchainView } from "ponder"; * * export const accountView = onchainView("account_view").as((qb) => * qb.select().from(account), * ); * * @param name - The view name in the database. * @param columns - [Optional] The view columns. * @returns The onchain view. */ export function onchainView( name: TName, ): ViewBuilder; export function onchainView< TName extends string, TColumns extends Record, >(name: TName, columns: TColumns): ManualViewBuilder; export function onchainView( name: string, columns?: Record, ): ViewBuilder | ManualViewBuilder { const schema = globalThis?.PONDER_NAMESPACE_BUILD?.schema; const view = pgViewWithSchema(name, columns, schema); // @ts-ignore view[onchain] = true; return view; } export const isPgEnumSym = Symbol.for("drizzle:isPgEnum"); export type OnchainEnum = { (): PgEnumColumnBuilderInitial<"", TValues>; ( name: TName, ): PgEnumColumnBuilderInitial; ( name?: TName, ): PgEnumColumnBuilderInitial; readonly enumName: string; readonly enumValues: TValues; readonly schema: string | undefined; /** @internal */ [isPgEnumSym]: true; } & { [onchain]: true }; export const onchainEnum = >( enumName: string, values: T | Writable, ): OnchainEnum> => { const schema = globalThis?.PONDER_NAMESPACE_BUILD?.schema; const e = pgEnumWithSchema(enumName, values, schema); // @ts-ignore e[onchain] = true; // @ts-ignore return e; }; /** @see https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-orm/src/pg-core/table.ts#L51 */ function pgTableWithSchema< name extends string, schema extends string | undefined, columns extends Record, >( name: name, columns: columns | ((columnTypes: PgColumnsBuilders) => columns), extraConfig: | ((self: BuildExtraConfigColumns) => PgTableExtraConfig) | undefined, schema: schema, baseName = name, ): PgTableWithColumns<{ name: name; schema: schema; columns: BuildColumns; dialect: "pg"; }> { const rawTable = new PgTable<{ name: name; schema: schema; columns: BuildColumns; dialect: "pg"; }>(name, schema, baseName); const { bigint: int8, text: _text, ...restColumns } = getPgColumnBuilders(); const parsedColumns: columns = typeof columns === "function" ? columns({ ...restColumns, int8, hex, bigint, bytes, text, json, jsonb }) : columns; const builtColumns = Object.fromEntries( Object.entries(parsedColumns).map(([name, colBuilderBase]) => { const colBuilder = colBuilderBase; // @ts-ignore colBuilder.setName(toSnakeCase(name)); // @ts-ignore const column = colBuilder.build(rawTable); // @ts-ignore rawTable[Symbol.for("drizzle:PgInlineForeignKeys")].push( // @ts-ignore ...colBuilder.buildForeignKeys(column, rawTable), ); return [name, column]; }), ) as unknown as BuildColumns; const builtColumnsForExtraConfig = Object.fromEntries( Object.entries(parsedColumns).map(([name, colBuilderBase]) => { const colBuilder = colBuilderBase as PgColumnBuilder; //@ts-ignore colBuilder.setName(toSnakeCase(name)); //@ts-ignore const column = colBuilder.buildExtraConfigColumn(rawTable); return [name, column]; }), ) as unknown as BuildExtraConfigColumns; const table = Object.assign(rawTable, builtColumns); //@ts-ignore table[Table.Symbol.Columns] = builtColumns; //@ts-ignore table[Table.Symbol.ExtraConfigColumns] = builtColumnsForExtraConfig; if (extraConfig) { //@ts-ignore table[PgTable.Symbol.ExtraConfigBuilder] = extraConfig as any; } return Object.assign(table, { enableRLS: () => { // @ts-ignore table[PgTable.Symbol.EnableRLS] = true; return table as PgTableWithColumns<{ name: name; schema: schema; columns: BuildColumns; dialect: "pg"; }>; }, }); } function pgViewWithSchema( name: string, selection: Record | undefined, schema: string | undefined, ): ViewBuilder | ManualViewBuilder { if (selection) { return new ManualViewBuilder(name, selection, schema); } return new ViewBuilder(name, schema); } function pgEnumWithSchema>( enumName: string, values: T | Writable, schema?: string, ): OnchainEnum> { const enumInstance: OnchainEnum> = Object.assign( ( name?: TName, ): PgEnumColumnBuilderInitial> => new PgEnumColumnBuilder(name ?? ("" as TName), enumInstance), { enumName, enumValues: values, schema, [isPgEnumSym]: true, [onchain]: true, } as const, ); return enumInstance; }