feat: deliver module database ABI by manifest
CI / verify (pull_request) Successful in 1m0s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 23s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s
CI / verify (pull_request) Successful in 1m0s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 23s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s
This commit is contained in:
@@ -3,7 +3,11 @@ import type {
|
||||
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";
|
||||
|
||||
@@ -83,3 +87,26 @@ export function moduleDrizzle<TSchema extends Record<string, unknown> = Record<s
|
||||
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<Record<string, AnyPgTable>>,
|
||||
): 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 });
|
||||
}
|
||||
|
||||
@@ -20,10 +20,12 @@ describe("the module-side database architecture", () => {
|
||||
});
|
||||
|
||||
it("keeps ABI v1 contracts free of ORM imports", async () => {
|
||||
const source = await text("packages/contracts/src/module-database-abi.ts");
|
||||
expect(source).not.toMatch(/(?:import|export).*drizzle-orm/u);
|
||||
expect(source).not.toMatch(/(?:import|export).*kysely/u);
|
||||
expect(source).not.toMatch(/(?:import|export).*\bpg\b/u);
|
||||
for (const file of ["module-database-abi.ts", "module-schema-metadata.ts"]) {
|
||||
const source = await text(`packages/contracts/src/${file}`);
|
||||
expect(source).not.toMatch(/(?:import|export).*drizzle-orm/u);
|
||||
expect(source).not.toMatch(/(?:import|export).*kysely/u);
|
||||
expect(source).not.toMatch(/(?:import|export).*\bpg\b/u);
|
||||
}
|
||||
});
|
||||
|
||||
it("makes Drizzle an optional ranged peer and keeps raw SQL independent from it", async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type ModuleDatabaseQueryResult,
|
||||
type ModuleDatabaseTransaction,
|
||||
} from "@ekklesia/contracts";
|
||||
import { and, eq, sql as drizzleSql } from "drizzle-orm";
|
||||
import { and, sql as drizzleSql, eq } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
customType,
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||
import { moduleDrizzle, type ModuleDrizzleDatabase } from "../src/drizzle.js";
|
||||
import {
|
||||
type ModuleDrizzleDatabase,
|
||||
moduleDrizzle,
|
||||
moduleDrizzleSchemaMetadata,
|
||||
} from "../src/drizzle.js";
|
||||
import { findEkklesiaDatabaseError } from "../src/index.js";
|
||||
|
||||
const bytea = customType<{ data: Uint8Array; driverData: Uint8Array }>({
|
||||
@@ -159,6 +163,24 @@ function setup(...outcomes: Outcome[]) {
|
||||
}
|
||||
|
||||
describe("moduleDrizzle query compilation and ABI mapping", () => {
|
||||
it("derives plain Ekklesia schema metadata with foreign-key targets", () => {
|
||||
const child = pgTable("calendar_children", {
|
||||
id: integer("id").primaryKey(),
|
||||
recordId: integer("record_id").references(() => records.id),
|
||||
});
|
||||
|
||||
const metadata = moduleDrizzleSchemaMetadata("calendar", { records, child });
|
||||
|
||||
expect(metadata).toEqual({
|
||||
moduleId: "calendar",
|
||||
tables: [
|
||||
{ name: "calendar_children", foreignKeys: [{ table: "calendar_records" }] },
|
||||
{ name: "calendar_records", foreignKeys: [] },
|
||||
],
|
||||
});
|
||||
expect(JSON.parse(JSON.stringify(metadata))).toEqual(metadata);
|
||||
});
|
||||
|
||||
it("compiles a select and decodes its positional result on the module side", async () => {
|
||||
const { db, executor } = setup(result([[7, "Launch"]]));
|
||||
await expect(
|
||||
|
||||
Reference in New Issue
Block a user