Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/transforms-poc-typescript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@common-grants/sdk": minor
---

Add a TypeScript proof-of-concept for the plugin transformation framework (issue #798), mirroring the Python PoC in PR #810. Plugin authors can now compile declarative ADR-0017 mapping objects into typed `(toCommon, fromCommon)` callables, validate `toCommon` output against an extended Zod schema, and attach those callables to a plugin via `definePlugin({ transformSchemas })`.

**New public surface (under `@common-grants/sdk/extensions`):**
- `buildTransforms()` — compiles a pair of mapping objects into typed `(toCommon, fromCommon)` callables with call-time structural validation. Optional `commonModel` Zod schema turns parse failures into `PluginError[]` instead of thrown exceptions.
- `TransformResult<T>` — unconditional `{ result, errors }` return shape per ADR-0022 Decision #7.
- `PluginError` — structured error class carrying `path`, `handler`, `sourceValue`, `cause` (ADR-0022 Decision #9).
- `transformFromMapping()`, `getFromPath()`, `DEFAULT_HANDLERS` — lower-level mapping runtime pieces; six built-in handlers (`const`, `field`, `match` / `switch` alias, `numberToString`, `stringToNumber`).
- `definePlugin()` accepts optional `meta: PluginMeta` and `transformSchemas: Partial<Record<ExtensibleSchemaName, ObjectSchemasInput>>`. Existing callers passing only `extensions` are unaffected.
- New supporting types: `Handler`, `ObjectSchemasInput`, `ObjectSchemas`, `PluginMeta`, `PluginCapability`, `ObjectMappings`, `PluginExtensionsObjectConfig`, `PluginExtensions`, `TransformSchemasInput`.

**Out of scope** (matches Python PoC; deferred to full SDK):
- Auto-generation of transforms from declarative `extensions.schemas[obj].mappings` inside `definePlugin()` (ADR-0022 Decision #6 TODO).
- Always-on `commonModel` validation inside `definePlugin()` — opt-in at `buildTransforms()` for now (ADR-0022 Decision #7).

Runnable example: `pnpm --filter @common-grants/sdk example:transforms` (round-trips a synthetic grants.gov record through `toCommon` and `fromCommon` with custom `join` / `split` handlers and extended-schema validation).
80 changes: 79 additions & 1 deletion lib/ts-sdk/__tests__/extensions/define-plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { describe, it, expect } from "vitest";
import { z } from "zod";
import { definePlugin, mergeExtensions, type SchemaExtensions } from "@/extensions";
import {
buildTransforms,
definePlugin,
mergeExtensions,
type PluginMeta,
type SchemaExtensions,
type TransformResult,
} from "@/extensions";
import { OpportunityBaseSchema } from "@/schemas/zod/models";
import { CustomFieldType } from "@/constants";

Expand Down Expand Up @@ -229,4 +236,75 @@ describe("definePlugin", () => {
expect(plugin.schemas.Opportunity).toBeDefined();
});
});

// ############################################################################
// ADR-0022 — meta and transformSchemas (PoC additions)
// ############################################################################

describe("meta", () => {
it("preserves meta on the returned plugin", () => {
const meta: PluginMeta = {
name: "grants.gov",
version: "1.0.0",
sourceSystem: "grants.gov",
capabilities: ["customFields", "transforms"],
};

const plugin = definePlugin({ extensions: {}, meta });

expect(plugin.meta).toBe(meta);
});

it("leaves meta undefined when not provided (backward compatibility)", () => {
const plugin = definePlugin({ extensions: {} });

expect(plugin.meta).toBeUndefined();
});
});

describe("transformSchemas", () => {
it("preserves transformSchemas on the returned plugin", () => {
const { toCommon, fromCommon } = buildTransforms({
toCommonMapping: { title: { field: "data.opportunity_title" } },
fromCommonMapping: { data: { opportunity_title: { field: "title" } } },
});

const plugin = definePlugin({
extensions: {},
transformSchemas: {
Opportunity: { toCommon, fromCommon },
},
});

expect(plugin.transformSchemas?.Opportunity?.toCommon).toBe(toCommon);
expect(plugin.transformSchemas?.Opportunity?.fromCommon).toBe(fromCommon);
});

it("invokes the stored transform callable via plugin.transformSchemas", () => {
const { toCommon, fromCommon } = buildTransforms({
toCommonMapping: { title: { field: "data.opportunity_title" } },
fromCommonMapping: { data: { opportunity_title: { field: "title" } } },
});

const plugin = definePlugin({
extensions: {},
transformSchemas: {
Opportunity: { toCommon, fromCommon },
},
});

const out = plugin.transformSchemas?.Opportunity?.toCommon?.({
data: { opportunity_title: "Hello" },
}) as TransformResult<{ title: string }>;

expect(out.errors).toEqual([]);
expect(out.result.title).toBe("Hello");
});

it("leaves transformSchemas undefined when not provided (backward compatibility)", () => {
const plugin = definePlugin({ extensions: {} });

expect(plugin.transformSchemas).toBeUndefined();
});
});
});
Loading
Loading