import type { ModuleDatabaseExecutor, ModuleDatabaseParameter, ModuleDatabaseQueryable, ModuleDatabaseTransaction, ModuleId, ModuleSchemaMetadataDefinition, } from "@ekklesia/contracts"; import { parseModuleSchemaMetadataDefinition } from "@ekklesia/contracts"; import { type AnyPgTable, getTableConfig } from "drizzle-orm/pg-core"; import { drizzle, type PgRemoteDatabase } from "drizzle-orm/pg-proxy"; import type { DrizzleConfig } from "drizzle-orm/utils"; type MaybePromise = Promise | T; type DrizzleSurface> = Pick< PgRemoteDatabase, "delete" | "insert" | "select" | "update" >; export type ModuleDrizzleTransaction< TSchema extends Record = Record, > = DrizzleSurface & { transaction( callback: (transaction: ModuleDrizzleTransaction) => MaybePromise, ): Promise; }; export type ModuleDrizzleDatabase = Record> = DrizzleSurface & { transaction( callback: (transaction: ModuleDrizzleTransaction) => MaybePromise, ): Promise; }; type TransactionRunner> = ( callback: (transaction: ModuleDrizzleTransaction) => MaybePromise, ) => Promise; function remoteDatabase>( executor: ModuleDatabaseQueryable, config: DrizzleConfig, runTransaction: TransactionRunner, ): ModuleDrizzleDatabase { const database = drizzle(async (text, parameters) => { const result = await executor.query({ text, // Drizzle performs column-specific parameter encoding before this callback. Values outside // ABI v1 remain unchanged so the ABI executor, not the adapter, rejects them. values: parameters as readonly ModuleDatabaseParameter[], // Drizzle maps selected fields by position. Array rows also remain correct when a join returns // duplicate PostgreSQL column names. rowMode: "array", }); return { rows: result.rows as unknown as unknown[][] }; }, config); return Object.freeze({ delete: database.delete.bind(database), insert: database.insert.bind(database), select: database.select.bind(database), transaction: runTransaction, update: database.update.bind(database), }) as ModuleDrizzleDatabase; } function transactionDatabase>( transaction: ModuleDatabaseTransaction, config: DrizzleConfig, ): ModuleDrizzleTransaction { return remoteDatabase(transaction, config, (callback) => transaction.savepoint((savepoint) => callback(transactionDatabase(savepoint, config))), ); } /** * Creates a module-owned Drizzle database backed exclusively by Module Database ABI v1 calls. * * Drizzle compiles and decodes on the module side. Core receives only SQL text, ordered parameter * values, and `rowMode: "array"`. Top-level transactions use the ABI transaction callback; nested * Drizzle transactions use ABI savepoints. */ export function moduleDrizzle = Record>( executor: ModuleDatabaseExecutor, config: DrizzleConfig = {}, ): ModuleDrizzleDatabase { return remoteDatabase(executor, config, (callback) => executor.transaction((transaction) => callback(transactionDatabase(transaction, config))), ); } /** Derives the plain ABI-v1 schema entrypoint from a module's own Drizzle tables. Drizzle stays on * this module-side adapter path; Core consumes only the returned strings and arrays. */ export function moduleDrizzleSchemaMetadata( moduleId: ModuleId, tables: Readonly>, ): ModuleSchemaMetadataDefinition { const metadata = Object.entries(tables) .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) .map(([_exportName, table]) => { const config = getTableConfig(table); if (config.schema !== undefined && config.schema !== "public") { throw new TypeError("Module schema metadata supports only the public PostgreSQL schema."); } return { name: config.name, foreignKeys: config.foreignKeys.map((foreignKey) => ({ table: getTableConfig(foreignKey.reference().foreignTable).name, })), }; }); return parseModuleSchemaMetadataDefinition({ moduleId, tables: metadata }); }