|
| 1 | +// api-contract-write-through-generated-server.ts — boots the GENERATED Order |
| 2 | +// routes (the deployed artifact) over HTTP and drives them against the #214 |
| 3 | +// write-through corpus. |
| 4 | +// |
| 5 | +// Mirrors api-contract-m2m-generated-server.ts, but the model is a WRITE-THROUGH |
| 6 | +// entity: Order has a writable `orders` table AND a read-only replica view |
| 7 | +// `v_order_with_customer` carrying the derived `customerName` (origin.passthrough |
| 8 | +// from Customer.name). It: |
| 9 | +// 1. runs the real codegen (runGen) over the write-through meta into a temp dir, |
| 10 | +// emitting Order's Drizzle table + the `.existing()` replica VIEW binding + |
| 11 | +// Order.routes.ts — which (post-fix) mounts mountCrudRoutes with `readView`; |
| 12 | +// 2. provisions the `customers` + `orders` tables AND the `v_order_with_customer` |
| 13 | +// SQL view the generated `.existing()` binding reads through; |
| 14 | +// 3. imports the EMITTED Order.routes.ts unmodified and mounts it. |
| 15 | +// |
| 16 | +// This proves the GENERATED write-through routes — the deployed artifact, not a |
| 17 | +// re-implementation — return the derived field on read-your-writes over HTTP. |
| 18 | + |
| 19 | +import Fastify, { type FastifyInstance } from "fastify"; |
| 20 | +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; |
| 21 | +import { dirname, join } from "node:path"; |
| 22 | +import { fileURLToPath, pathToFileURL } from "node:url"; |
| 23 | +import { runGen, defineConfig } from "@metaobjectsdev/codegen-ts"; |
| 24 | +import { entityFile, routesFile } from "@metaobjectsdev/codegen-ts/generators"; |
| 25 | +import pg from "pg"; |
| 26 | +import { executeSql } from "./postgres-sql.ts"; |
| 27 | +import { loadMetadataFile } from "./load-metadata.ts"; |
| 28 | + |
| 29 | +export interface WriteThroughSeed { |
| 30 | + customers: Array<{ id: number; name: string }>; |
| 31 | + orders: Array<{ id: number; customerId: number }>; |
| 32 | +} |
| 33 | + |
| 34 | +export interface GeneratedWriteThroughServerHandle { |
| 35 | + baseUrl: string; |
| 36 | + applySeed(seed: WriteThroughSeed): Promise<void>; |
| 37 | + close(): Promise<void>; |
| 38 | +} |
| 39 | + |
| 40 | +export async function startGeneratedWriteThroughServer( |
| 41 | + connectionUri: string, |
| 42 | + metaPath: string, |
| 43 | +): Promise<GeneratedWriteThroughServerHandle> { |
| 44 | + const here = dirname(fileURLToPath(import.meta.url)); |
| 45 | + const genTmpRoot = join(here, "..", ".gen-tmp"); |
| 46 | + mkdirSync(genTmpRoot, { recursive: true }); |
| 47 | + const tmp = mkdtempSync(join(genTmpRoot, "api-contract-write-through-")); |
| 48 | + |
| 49 | + // 1. Emit the real artifacts for the write-through model. |
| 50 | + const root = await loadMetadataFile(metaPath); |
| 51 | + const lr = await runGen({ |
| 52 | + config: defineConfig({ |
| 53 | + outDir: tmp, |
| 54 | + extStyle: "none", |
| 55 | + dbImport: "./db", |
| 56 | + dialect: "postgres", |
| 57 | + apiPrefix: "/api", |
| 58 | + generators: [entityFile(), routesFile()], |
| 59 | + }), |
| 60 | + metadata: root, |
| 61 | + }); |
| 62 | + if (lr.warnings.length > 0) { |
| 63 | + throw new Error(`codegen produced warnings: ${lr.warnings.join("; ")}`); |
| 64 | + } |
| 65 | + |
| 66 | + // 2. db module the emitted routes import (`import { db } from "./db"`). |
| 67 | + const bigintTypesImport = pathToFileURL(join(here, "pg-bigint-number-types.ts")).href; |
| 68 | + const dbModule = ` |
| 69 | +import { drizzle } from "drizzle-orm/node-postgres"; |
| 70 | +import pg from "pg"; |
| 71 | +import { bigintAsNumberTypes } from ${JSON.stringify(bigintTypesImport)}; |
| 72 | +export const pool = new pg.Pool({ connectionString: ${JSON.stringify(connectionUri)}, types: bigintAsNumberTypes }); |
| 73 | +export const db = drizzle(pool); |
| 74 | +`; |
| 75 | + writeFileSync(join(tmp, "db.ts"), dbModule, "utf8"); |
| 76 | + |
| 77 | + // 3. Provision the base tables (snake_case, matching the EMITTED Drizzle table) |
| 78 | + // AND the replica VIEW the generated `.existing()` binding reads through. The |
| 79 | + // view columns (id / customer_id / customer_name) match orderView's physical |
| 80 | + // column names, so a read routed through it carries the derived customerName. |
| 81 | + await executeSql(connectionUri, ` |
| 82 | + CREATE TABLE IF NOT EXISTS "customers" ("id" bigserial PRIMARY KEY, "name" varchar(80) NOT NULL); |
| 83 | + CREATE TABLE IF NOT EXISTS "orders" ("id" bigserial PRIMARY KEY, "customer_id" bigint NOT NULL); |
| 84 | + CREATE OR REPLACE VIEW "v_order_with_customer" AS |
| 85 | + SELECT o."id" AS "id", o."customer_id" AS "customer_id", c."name" AS "customer_name" |
| 86 | + FROM "orders" o JOIN "customers" c ON o."customer_id" = c."id"; |
| 87 | + `); |
| 88 | + |
| 89 | + // 4. Import the EMITTED Order route file unmodified and mount it. |
| 90 | + const orderRoutes = (await import(pathToFileURL(join(tmp, "Order.routes.ts")).href)) as { |
| 91 | + orderRoutes: (f: FastifyInstance) => Promise<void>; |
| 92 | + }; |
| 93 | + const dbMod = (await import(pathToFileURL(join(tmp, "db.ts")).href)) as { pool: pg.Pool }; |
| 94 | + |
| 95 | + const fastify = Fastify(); |
| 96 | + await fastify.register(orderRoutes.orderRoutes); |
| 97 | + await fastify.ready(); |
| 98 | + const baseUrl = await fastify.listen({ host: "127.0.0.1", port: 0 }); |
| 99 | + |
| 100 | + return { |
| 101 | + baseUrl, |
| 102 | + applySeed: async (seed: WriteThroughSeed) => { |
| 103 | + await seedWriteThrough(connectionUri, seed); |
| 104 | + }, |
| 105 | + close: async () => { |
| 106 | + await fastify.close(); |
| 107 | + await dbMod.pool.end(); |
| 108 | + rmSync(tmp, { recursive: true, force: true }); |
| 109 | + }, |
| 110 | + }; |
| 111 | +} |
| 112 | + |
| 113 | +/** Truncate + insert the write-through corpus seed (base tables only — the |
| 114 | + * derived customerName is produced by the view join on read). */ |
| 115 | +export async function seedWriteThrough(connectionUri: string, seed: WriteThroughSeed): Promise<void> { |
| 116 | + await executeSql(connectionUri, `TRUNCATE TABLE "customers","orders" RESTART IDENTITY CASCADE;`); |
| 117 | + for (const c of seed.customers) |
| 118 | + await executeSql(connectionUri, `INSERT INTO "customers" ("id","name") VALUES (${c.id}, ${str(c.name)})`); |
| 119 | + for (const o of seed.orders) |
| 120 | + await executeSql(connectionUri, `INSERT INTO "orders" ("id","customer_id") VALUES (${o.id}, ${o.customerId})`); |
| 121 | +} |
| 122 | + |
| 123 | +function str(v: string): string { |
| 124 | + return `'${v.replace(/'/g, "''")}'`; |
| 125 | +} |
0 commit comments