fix: keep Drizzle out of every published declaration (ADR 0053)
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 37s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 28s
CI / deploy-smoke (same-origin) (pull_request) Successful in 43s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s

@ekklesia/contracts still emitted a type-only import of
drizzle-orm/node-postgres for the legacy ModuleDatabase facade, so a
standalone module type-checked only because skipLibCheck hid it.

- ModuleDatabase, LegacyModuleRuntimeServices and
  AnyModuleRuntimeServices move to core/src/modules/database.ts, beside
  the facade Core builds; the facade itself is unchanged.
- contracts' runtime, route, and services generics no longer name an
  ORM: the database type is a plain parameter defaulting to the ABI v1
  executor, and the legacy definition's database is opaque until Core
  specializes it. Core and apps/api name the legacy types from Core.
- contracts drops its drizzle-orm optional peer and devDependency;
  Drizzle stays public only through @ekklesia/module-database/drizzle.
- The facade's type-level test moves from contracts to Core.
- Regression proof: the standalone fixture type-checks with
  skipLibCheck: false against the packed tarballs, with no drizzle-orm
  installed (the smoke asserts both). Against the previous contracts
  tarball the same check fails with TS2307 on drizzle-orm/node-postgres.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-08 07:40:43 -05:00
co-authored by Claude Opus 5.5
parent 89d5d0f2da
commit aaa7ebfd84
31 changed files with 190 additions and 178 deletions
@@ -1,95 +0,0 @@
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { describe, expect, it } from "vitest";
import type { ModuleDatabase, ModuleRuntimeServices } from "../src/index.js";
const testTable = pgTable("contracts_module_database_test", {
id: integer("id").primaryKey(),
name: text("name").notNull(),
});
/**
* Never invoked — exists solely so `pnpm typecheck` (part of `pnpm verify`) proves `ModuleDatabase`
* preserves Drizzle's own table-aware argument and result inference for every required operation,
* with no `any`/`unknown` erasure anywhere in the chain. A regression that widened any of these
* signatures to `any` would still compile a *call* to the method but would break one of the
* explicit row-shape assertions below.
*/
function typeOnlyOperationInference(database: ModuleDatabase): void {
const selected = database.select({ id: testTable.id, name: testTable.name }).from(testTable);
const selectedRow: { id: number; name: string } = {} as Awaited<typeof selected>[number];
void selectedRow;
const distinct = database.selectDistinct().from(testTable);
const distinctRow: { id: number; name: string } = {} as Awaited<typeof distinct>[number];
void distinctRow;
const distinctOn = database.selectDistinctOn([testTable.name]).from(testTable);
const distinctOnRow: { id: number; name: string } = {} as Awaited<typeof distinctOn>[number];
void distinctOnRow;
const inserted = database.insert(testTable).values({ id: 1, name: "a" });
void inserted;
const updated = database.update(testTable).set({ name: "b" });
void updated;
const deleted = database.delete(testTable);
void deleted;
const executed = database.execute("select 1");
void executed;
void database.transaction(async (tx) => {
const rows: Array<{ id: number }> = await tx.select({ id: testTable.id }).from(testTable);
return rows;
});
}
void typeOnlyOperationInference;
/**
* Never invoked — proves at typecheck time that `ModuleDatabase` structurally excludes every raw
* PostgreSQL/Drizzle escape hatch a module must never reach through this facade. If any of these
* properties were ever (re)added to the picked surface, the corresponding `@ts-expect-error` would
* itself become a type error ("unused '@ts-expect-error' directive"), failing `pnpm typecheck`.
*/
function typeOnlyExclusions(database: ModuleDatabase): void {
// @ts-expect-error `$client` (the raw pg client/pool) is not part of the public facade.
void database.$client;
// @ts-expect-error `_` (internal session/dialect/schema state) is not part of the public facade.
void database._;
// @ts-expect-error `query` (a schema-bound relational query builder) is not part of the public facade.
void database.query;
// @ts-expect-error a raw connection pool is not part of the public facade.
void database.pool;
// @ts-expect-error a raw connection string is not part of the public facade.
void database.connectionString;
// @ts-expect-error migration operations are not part of the public facade.
void database.migrate;
}
void typeOnlyExclusions;
/** Never invoked — proves `ModuleRuntimeServices` has exactly one field, `database`, typed as
* `ModuleDatabase`, with no storage/jobs/configuration/secrets/authorization field this phase. */
function typeOnlyServicesShape(services: ModuleRuntimeServices): void {
const database: ModuleDatabase = services.database;
void database;
// @ts-expect-error `storage` does not exist on `ModuleRuntimeServices` yet.
void services.storage;
// @ts-expect-error `jobs` does not exist on `ModuleRuntimeServices` yet.
void services.jobs;
// @ts-expect-error `authorization` does not exist on `ModuleRuntimeServices` yet.
void services.authorization;
}
void typeOnlyServicesShape;
describe("ModuleDatabase", () => {
it("is a real, importable type contract enforced by pnpm typecheck", () => {
// The meaningful assertions for this module-level type contract are compile-time
// (`typeOnlyOperationInference`, `typeOnlyExclusions`, `typeOnlyServicesShape` above, checked
// by `pnpm typecheck`); this keeps the file a genuine, executed Vitest suite rather than a
// file Vitest would otherwise skip for declaring no runnable test.
expect(typeof typeOnlyOperationInference).toBe("function");
expect(typeof typeOnlyExclusions).toBe("function");
expect(typeof typeOnlyServicesShape).toBe("function");
});
});