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
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:
co-authored by
Claude Opus 5.5
parent
89d5d0f2da
commit
aaa7ebfd84
@@ -0,0 +1,96 @@
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { LegacyModuleRuntimeServices, ModuleDatabase } from "../src/modules/database.js";
|
||||
|
||||
const testTable = pgTable("core_module_database_type_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 a legacy module's `ModuleRuntimeServices` has exactly one field,
|
||||
* `database`, typed as `ModuleDatabase`, with no storage/jobs/configuration/secrets/authorization
|
||||
* field this phase. */
|
||||
function typeOnlyServicesShape(services: LegacyModuleRuntimeServices): void {
|
||||
const database: ModuleDatabase = services.database;
|
||||
void database;
|
||||
// @ts-expect-error `storage` does not exist on the services yet.
|
||||
void services.storage;
|
||||
// @ts-expect-error `jobs` does not exist on the services yet.
|
||||
void services.jobs;
|
||||
// @ts-expect-error `authorization` does not exist on the services 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");
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,8 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import type {
|
||||
ModuleDatabase,
|
||||
ModuleRealtimePublisher,
|
||||
ModuleRuntimeServices,
|
||||
} from "@ekklesia/contracts";
|
||||
import type { ModuleRealtimePublisher } from "@ekklesia/contracts";
|
||||
import type { LegacyModuleRuntimeServices, ModuleDatabase } from "../src/modules/database.js";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { ModuleRuntimeError, startModuleRuntime } from "../src/modules/runtime.js";
|
||||
|
||||
@@ -22,7 +19,7 @@ afterEach(async () => {
|
||||
);
|
||||
});
|
||||
|
||||
function fakeServices(): ModuleRuntimeServices {
|
||||
function fakeServices(): LegacyModuleRuntimeServices {
|
||||
return Object.freeze({ database: Object.freeze({}) as ModuleDatabase });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import type { ModuleConfiguration, ModuleDatabaseExecutor } from "@ekklesia/contracts";
|
||||
import type {
|
||||
AnyModuleRuntimeServices,
|
||||
ModuleConfiguration,
|
||||
LegacyModuleRuntimeServices,
|
||||
ModuleDatabase,
|
||||
ModuleDatabaseExecutor,
|
||||
ModuleRuntimeServices,
|
||||
} from "@ekklesia/contracts";
|
||||
} from "../src/modules/database.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { discoverModules } from "../src/modules/discovery.js";
|
||||
import {
|
||||
@@ -151,7 +150,7 @@ function fakeModuleDatabase(): ModuleDatabase {
|
||||
}
|
||||
|
||||
function fakeModuleRuntimeServices(): ModuleRuntimeServicesLookup {
|
||||
const services = new Map<string, ModuleRuntimeServices>();
|
||||
const services = new Map<string, LegacyModuleRuntimeServices>();
|
||||
const database = fakeModuleDatabase();
|
||||
return new Proxy(services, {
|
||||
get(target, property, receiver) {
|
||||
|
||||
Reference in New Issue
Block a user