|
| 1 | +import { describe, test, expect, afterAll } from "bun:test"; |
| 2 | +import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises"; |
| 3 | +import { tmpdir } from "node:os"; |
| 4 | +import { join } from "node:path"; |
| 5 | +import { runBaseline, runOfflineGenerate } from "../src/commands/migrate.js"; |
| 6 | + |
| 7 | +// #157 — the offline migrate paths (baseline + offline generate) must thread the |
| 8 | +// consumer `providers` from metaobjects.config.ts into loadMemory, exactly like |
| 9 | +// `meta gen` and the DB migrate paths do. Otherwise a project that registers a |
| 10 | +// custom subtype via a config provider hits `Unknown type <subtype>` on offline |
| 11 | +// `meta migrate`, forcing a "strip the custom nodes" workaround. |
| 12 | + |
| 13 | +const dirs: string[] = []; |
| 14 | +afterAll(async () => { |
| 15 | + for (const d of dirs) await rm(d, { recursive: true, force: true }); |
| 16 | +}); |
| 17 | + |
| 18 | +// A config that registers a custom `validator.geocheck` subtype via a provider — |
| 19 | +// mirrors how an adopter ships a custom subtype to gen/migrate. A validator is |
| 20 | +// used (not a field) so it exercises provider-threading without also depending on |
| 21 | +// how the migrate column-mapper handles a novel physical type. |
| 22 | +const CUSTOM_CONFIG = [ |
| 23 | + `import { defineConfig } from "@metaobjectsdev/codegen-ts";`, |
| 24 | + `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`, |
| 25 | + `import { TypeId, TYPE_VALIDATOR, MetaValidator } from "@metaobjectsdev/metadata";`, |
| 26 | + `const geoProvider = {`, |
| 27 | + ` id: "test-geocheck",`, |
| 28 | + ` dependencies: ["metaobjects-core-types"],`, |
| 29 | + ` registerTypes(registry) {`, |
| 30 | + ` registry.register({`, |
| 31 | + ` typeId: new TypeId(TYPE_VALIDATOR, "geocheck"),`, |
| 32 | + ` description: "A custom validator",`, |
| 33 | + ` factory: (typeId, name) => new MetaValidator(typeId, name),`, |
| 34 | + ` childRules: [],`, |
| 35 | + ` attributes: [],`, |
| 36 | + ` });`, |
| 37 | + ` },`, |
| 38 | + `};`, |
| 39 | + `export default defineConfig({`, |
| 40 | + ` outDir: "out",`, |
| 41 | + ` dialect: "postgres",`, |
| 42 | + ` generators: [entityFile()],`, |
| 43 | + ` providers: [geoProvider],`, |
| 44 | + `});`, |
| 45 | +].join("\n"); |
| 46 | + |
| 47 | +// An entity (writable table) whose `name` field carries the custom validator. |
| 48 | +// The `validator.geocheck` child resolves ONLY when the config provider loads — |
| 49 | +// so an offline migrate that doesn't thread providers fails at metadata load. |
| 50 | +const CUSTOM_META = { |
| 51 | + "metadata.root": { |
| 52 | + package: "acme::geo", |
| 53 | + children: [ |
| 54 | + { |
| 55 | + "object.entity": { |
| 56 | + name: "Place", |
| 57 | + children: [ |
| 58 | + { "field.long": { name: "id" } }, |
| 59 | + { |
| 60 | + "field.string": { |
| 61 | + name: "name", |
| 62 | + children: [{ "validator.geocheck": { name: "chk" } }], |
| 63 | + }, |
| 64 | + }, |
| 65 | + { "source.rdb": { name: "src", "@table": "places" } }, |
| 66 | + { |
| 67 | + "identity.primary": { |
| 68 | + name: "pk", |
| 69 | + "@fields": ["id"], |
| 70 | + "@generation": "increment", |
| 71 | + }, |
| 72 | + }, |
| 73 | + ], |
| 74 | + }, |
| 75 | + }, |
| 76 | + ], |
| 77 | + }, |
| 78 | +}; |
| 79 | + |
| 80 | +async function customProject(): Promise<string> { |
| 81 | + const root = await mkdtemp(join(tmpdir(), "mts-migprov-")); |
| 82 | + dirs.push(root); |
| 83 | + await mkdir(join(root, "metaobjects"), { recursive: true }); |
| 84 | + await writeFile( |
| 85 | + join(root, "metaobjects", "meta.json"), |
| 86 | + JSON.stringify(CUSTOM_META), |
| 87 | + "utf8", |
| 88 | + ); |
| 89 | + await writeFile(join(root, "metaobjects.config.ts"), CUSTOM_CONFIG, "utf8"); |
| 90 | + return root; |
| 91 | +} |
| 92 | + |
| 93 | +describe("offline migrate threads config providers (#157)", () => { |
| 94 | + test("runBaseline loads a config-registered custom subtype (no strip workaround)", async () => { |
| 95 | + const root = await customProject(); |
| 96 | + const code = await runBaseline( |
| 97 | + { dialect: "postgres", outDir: "./.metaobjects/migrations", fromDb: false } as any, |
| 98 | + root, |
| 99 | + ); |
| 100 | + expect(code).toBe(0); |
| 101 | + }); |
| 102 | + |
| 103 | + test("runOfflineGenerate loads a config-registered custom subtype", async () => { |
| 104 | + const root = await customProject(); |
| 105 | + // baseline first (also needs the provider) so there is a snapshot to diff. |
| 106 | + await runBaseline( |
| 107 | + { dialect: "postgres", outDir: "./.metaobjects/migrations", fromDb: false } as any, |
| 108 | + root, |
| 109 | + ); |
| 110 | + const code = await runOfflineGenerate( |
| 111 | + { |
| 112 | + dialect: "postgres", |
| 113 | + outDir: "./.metaobjects/migrations", |
| 114 | + allow: [], |
| 115 | + onAmbiguous: "error", |
| 116 | + } as any, |
| 117 | + root, |
| 118 | + ); |
| 119 | + expect(code).toBe(0); |
| 120 | + }); |
| 121 | +}); |
0 commit comments