/** * Shared foundation types for the extensions module. * * Only types referenced by multiple files live here. Types specific to a * single utility function are co-located with that function. * * @module @common-grants/sdk/extensions */ import { z } from "zod"; import type { CustomField, CustomFieldType } from "../types"; /** * Specification for a custom field to be registered on a schema, with an optional * Zod schema to validate the value property. * * @example * The following `CustomFieldSpec` object: * * ```typescript * const spec: CustomFieldSpec = { * name: "Legacy ID", * fieldType: "integer", * value: z.number().int(), * description: "An integer ID for the opportunity, needed for compatibility with legacy systems", * }; * ``` * Corresponds to the following custom field object: * ```json * { * "name": "Legacy ID", * "fieldType": "integer", * "value": 12345, * "description": "An integer ID for the opportunity, needed for compatibility with legacy systems", * } * ``` */ export interface CustomFieldSpec { /** Optional display name; used as the default for CustomField.name when provided, otherwise the record key is used */ name?: string; /** The JSON schema type for the field */ fieldType: CustomFieldType; /** Optional Zod schema to validate the value property. Defaults based on fieldType */ value?: z.ZodType; /** Optional description; used as the default for CustomField.description when present */ description?: string; } /** * The 10-value enum of supported custom filter types. * * Each value maps 1:1 to a base filter type with auto-derived operators. * Operators are NOT authored — they are derived from the filterType at * classification time (e.g. `booleanComparison` → `eq|neq`). * * NOTE: Build against these authoritative `*Comparison` names. * Terse names (`"string"`, `"number"`) are NOT valid — always use the full * `*Comparison` / `*Array` / `*Range` form. */ export type CustomFilterType = "stringComparison" | "stringArray" | "numberComparison" | "numberArray" | "numberRange" | "booleanComparison" | "dateComparison" | "dateRange" | "moneyComparison" | "moneyRange"; /** * Specification for a custom filter to be registered on a route method. * * The record key is the filter name; no separate `name` field is needed. * Operators are auto-derived from `filterType`; no Zod value schema is * authored here (contrast with `CustomFieldSpec.value`). * * @example * ```typescript * const agencyFilter: CustomFilterSpec = { * filterType: "stringArray", * description: "Filter by funding agency", * }; * ``` */ export interface CustomFilterSpec { /** The filter type — determines the allowed operators and value shape */ filterType: CustomFilterType; /** Optional human-readable description of the filter */ description?: string; } /** * Filter declarations for a single route method (e.g. `search`). * * Filters are keyed by name; the record value is a `CustomFilterSpec`. */ export interface RouteDeclarations { filters?: Record; } /** Resource names the SDK exposes. Grows as API resources land. */ export type ResourceName = "opportunities"; /** Route methods that accept custom filters. Widen only when a method actually takes filters. */ export type RouteMethod = "search"; /** Method → declarations map for a single resource. */ export type RouteMethods = Partial>; /** * Route map for custom filter declarations: resource → method → declarations. * * Passed as `routes` in `definePlugin()`. Filters attach to resource methods * (which vary asymmetrically across schemas), not to a schema. Both key levels * are closed unions, so a misspelled resource or method is a compile error. * * @example * ```typescript * const routes: PluginRoutes = { * opportunities: { * search: { * filters: { * agency: { filterType: "stringArray" }, * fundingProgram: { filterType: "stringComparison" }, * }, * }, * }, * }; * ``` */ export type PluginRoutes = Partial>; /** * The expected Zod type for a `customFields` property on an extensible schema. * * Matches `z.record(z.string(), CustomFieldSchema).nullish()`, which is the pattern used * by all base schemas in the SDK. The type is intentionally permissive about * the wrapping order (e.g. `.nullish()` vs `.optional().nullable()`) by * constraining only the output type. * * @internal */ type CustomFieldsZodType = z.ZodType | null | undefined>; /** * A Zod object schema whose shape includes a `customFields` property * typed as a record of `CustomField` values. * * Used to constrain the `baseSchema` parameter of `withCustomFields()` so that * schemas without a properly typed `customFields` are rejected at compile time. */ export type HasCustomFields = z.ZodObject<{ customFields: CustomFieldsZodType; } & z.ZodRawShape>; /** * An object with an optional `customFields` property. * * Used to constrain the first parameter of `getCustomFieldValue()` so that * only objects with a `customFields` property are accepted. */ export interface ExtensibleObject { customFields?: Record | null; } /** * The definitive list of base model names that support `customFields` extensions. * * @todo Add schemas here if they support `customFields` extensions and * map them to Zod schemas in `EXTENSIBLE_SCHEMA_MAP`. */ export type ExtensibleSchemaName = "Opportunity"; /** * Maps each `ExtensibleSchemaName` to its base Zod schema constant. * * This is the source of truth for extensible schema type inference. * The `PluginSchemas` type derives from `typeof EXTENSIBLE_SCHEMA_MAP` so that * the two stay in sync automatically. * * @todo Keep this map in sync with `ExtensibleSchemaName`. * * @internal */ export declare const EXTENSIBLE_SCHEMA_MAP: { readonly Opportunity: z.ZodObject<{ id: z.ZodUUID; title: z.ZodString; status: z.ZodObject<{ value: z.ZodEnum<{ custom: "custom"; forecasted: "forecasted"; open: "open"; closed: "closed"; }>; customValue: z.ZodOptional>; description: z.ZodOptional>; }, z.core.$strip>; description: z.ZodString; funding: z.ZodOptional>; totalAmountAvailable: z.ZodOptional>>; minAwardAmount: z.ZodOptional>>; maxAwardAmount: z.ZodOptional>>; minAwardCount: z.ZodOptional>; maxAwardCount: z.ZodOptional>; estimatedAwardCount: z.ZodOptional>; }, z.core.$strict>>>; keyDates: z.ZodOptional>; eventType: z.ZodLiteral<"singleDate">; date: z.ZodPreprocess>>; time: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ name: z.ZodString; description: z.ZodOptional>; eventType: z.ZodLiteral<"dateRange">; startDate: z.ZodPreprocess>>; startTime: z.ZodOptional>>; endDate: z.ZodPreprocess>>; endTime: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ name: z.ZodString; eventType: z.ZodLiteral<"other">; details: z.ZodOptional>; description: z.ZodOptional>; }, z.core.$strip>], "eventType">>>; closeDate: z.ZodOptional>; eventType: z.ZodLiteral<"singleDate">; date: z.ZodPreprocess>>; time: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ name: z.ZodString; description: z.ZodOptional>; eventType: z.ZodLiteral<"dateRange">; startDate: z.ZodPreprocess>>; startTime: z.ZodOptional>>; endDate: z.ZodPreprocess>>; endTime: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ name: z.ZodString; eventType: z.ZodLiteral<"other">; details: z.ZodOptional>; description: z.ZodOptional>; }, z.core.$strip>], "eventType">>>; otherDates: z.ZodOptional>; eventType: z.ZodLiteral<"singleDate">; date: z.ZodPreprocess>>; time: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ name: z.ZodString; description: z.ZodOptional>; eventType: z.ZodLiteral<"dateRange">; startDate: z.ZodPreprocess>>; startTime: z.ZodOptional>>; endDate: z.ZodPreprocess>>; endTime: z.ZodOptional>>; }, z.core.$strip>, z.ZodObject<{ name: z.ZodString; eventType: z.ZodLiteral<"other">; details: z.ZodOptional>; description: z.ZodOptional>; }, z.core.$strip>], "eventType">>>>; }, z.core.$strict>>>; acceptedApplicantTypes: z.ZodOptional; customValue: z.ZodOptional>; description: z.ZodOptional>; }, z.core.$strict>>>>; source: z.ZodOptional>; customFields: z.ZodOptional; schema: z.ZodOptional>; value: z.ZodUnknown; description: z.ZodOptional>; }, z.core.$strip>>>>; createdAt: z.ZodPreprocess>>; lastModifiedAt: z.ZodPreprocess>>; }, z.core.$strip>; }; /** * Features a plugin can declare in `PluginMeta.capabilities`. */ export type PluginCapability = "customFields" | "customFilters" | "transforms"; /** * Handler signature for transform mapping handlers. * * - First arg: the source data being transformed (where field paths resolve from). * - Second arg: the handler argument from the mapping spec. * - Return: the transformed value. * * @remarks * One contract custom-handler authors should respect: * * **Do not throw `Error`s whose `.message` embeds source data when that * data may contain PII.** `buildTransforms()` wraps a handler exception's * message verbatim into the resulting `TransformError.message`, which is * enumerable on `Error.prototype` and rendered by `util.inspect` / * `console.log(err)`. The SDK does not redact by default — * `TransformError.sourceValue` and `.cause` are enumerable, and * `.message` flows through verbatim. The built-in `stringToNumber` handler * follows this rule by throwing a generic "cannot convert source value to a * number" message; see the README's `TransformError` PII warning for the * adopter-side redaction pattern. */ export type Handler = (data: unknown, arg: unknown) => unknown; /** * Unconditional return shape for `toCommon` / `fromCommon`. * * `result` is the transformed value (may be partial on handler error or validation * failure). `errors` is the aggregated `TransformError` list, empty on full success. * * Consumers apply their own strict-vs-lenient rule — strict adopters treat any * non-empty `errors` as failure; lenient adopters use `result` despite warnings * and inspect `errors` for context. */ export interface TransformResult { result: T; errors: TransformError[]; } /** * Structured transformation error. * * Carries field path, handler name, source value, and underlying cause so * consumers can reason about failures programmatically without parsing error text. * * @remarks * **The SDK does not redact by default.** * `sourceValue` and `cause` are plain enumerable fields and flow through * `JSON.stringify(err)`, `util.inspect(err)`, and any logger that enumerates * own properties. When populated by `buildTransforms()`, `sourceValue` is the * entire input record passed to `toCommon` / `fromCommon` — not just the * value at the failing field — so adopters whose source data may contain PII * must redact before logging. * * Partial-redaction pattern (strips `sourceValue` and `cause` only — see * caveat on `message` below): * ```ts * const partiallySafe = { * name: err.name, * // CAUTION: `message` is data-bearing on the Zod-validation path. * // Strip or transform it before logging if your source data may contain PII. * message: err.message, * path: err.path, * handler: err.handler, * }; * ``` * * `TransformError.message` is data-bearing on the Zod-validation path * (`buildTransforms({ commonSchema })`): Zod's default error map embeds the * received runtime value into `issue.message`, which flows verbatim into * `TransformError.message`. Adopters whose source data may contain PII must redact * `message` alongside `sourceValue` and `cause`. Full-message sanitization is * tracked under #744. */ export declare class TransformError extends Error { /** Dot-notation field path where the error occurred, if known. */ path?: string; /** Name of the handler that raised, if applicable. */ handler?: string; /** The source value that triggered the error (may contain PII — redact before logging). */ sourceValue?: unknown; /** Underlying cause of the error, if any (may contain PII — redact before logging). */ cause?: unknown; constructor(message: string, options?: { path?: string; handler?: string; sourceValue?: unknown; cause?: unknown; }); } /** * Structured error raised by custom-filter validation (`validateRoutes`, * `validateFilterCall`, `classifyFilters`). * * Carries field path, handler name, source value, and underlying cause for * programmatic handling. `sourceValue` and `cause` may carry PII — redact * before logging. */ export declare class FilterError extends Error { /** Dot-notation field path where the error occurred, if known. */ path?: string; /** Name of the handler that raised, if applicable. */ handler?: string; /** The source value that triggered the error (may contain PII — redact before logging). */ sourceValue?: unknown; /** Underlying cause of the error, if any (may contain PII — redact before logging). */ cause?: unknown; constructor(message: string, options?: { path?: string; handler?: string; sourceValue?: unknown; cause?: unknown; }); } /** * Mappings authoring path: declarative `mappings` compiled by `buildTransforms()` * inside `definePlugin()`. Requires a `sourceSchema`; forbids hand-written * `toCommon` / `fromCommon`. */ export interface MappingsSchemaInput { /** Custom fields to attach via `withCustomFields()`. */ customFields?: Record; /** Source-system Zod schema (the shape a source system returns). */ sourceSchema: z.ZodType; /** Declarative mappings compiled into transforms by `definePlugin()`. */ mappings: SchemaMappings; toCommon?: never; fromCommon?: never; } /** * Functions authoring path: hand-written `toCommon` / `fromCommon`. Requires a * `sourceSchema` and both directions; forbids declarative `mappings`. * * The function slots are loose on the input side (`source: any`): a flat, * multi-key `definePlugin()` cannot infer a per-entry common type to check an * inline function, so the parameter falls to `any`. The slot still pins the * `TransformResult` envelope (a function returning a non-`TransformResult` is * rejected). Authors recover full typing with the `ToCommon` / `FromCommon` * helper types, and the resolved consumer-facing types are always correct. */ export interface FunctionsSchemaInput { /** Custom fields to attach via `withCustomFields()`. */ customFields?: Record; /** Source-system Zod schema (the shape a source system returns). */ sourceSchema: z.ZodType; mappings?: never; /** Map a source record to the common-schema shape. */ toCommon: (source: any) => TransformResult; /** Map a common-schema record back to the source shape. */ fromCommon: (common: any) => TransformResult; } /** Schema-only path: custom fields, no transforms. Forbids the other two paths. */ export interface SchemaOnlyInput { /** Custom fields to attach via `withCustomFields()`. */ customFields?: Record; sourceSchema?: never; mappings?: never; toCommon?: never; fromCommon?: never; } /** * Author-provided input for a single extensible object, passed inside * `DefinePluginOptions.schemas`. * * An exclusive choice between three paths: * - {@link MappingsSchemaInput}: `sourceSchema` + declarative `mappings`. * - {@link FunctionsSchemaInput}: `sourceSchema` + hand-written `toCommon` and * `fromCommon` (both required). * - {@link SchemaOnlyInput}: `customFields` only, no transforms. * * Both transform paths require a `sourceSchema`, so a transform can always be * validated against the source shape. Supplying both `mappings` and functions, * a single transform direction, or a transform without a `sourceSchema`, is a * compile error (the `?: never` slots). `commonSchema` is intentionally absent; * `definePlugin()` derives it from `customFields` during compilation. */ export type SchemaInput = MappingsSchemaInput | FunctionsSchemaInput | SchemaOnlyInput; /** * Compiled output for a schema-only entry — no transforms configured. * * Produced by `definePlugin()` for entries that declare only `customFields` * (or nothing at all). The `commonSchema` is the fully extended Zod schema. * `customFields` is kept on the entry so consumers can inspect the specs that * were used to build it. */ export interface SchemaOnly { commonSchema: z.ZodType; /** Custom field specs that were used to extend the base schema, kept for inspection. */ customFields?: Record; } /** * Compiled output for a schema entry with bidirectional transforms. * * Produced by `definePlugin()` for entries that declare either `mappings` or * explicit `toCommon` / `fromCommon` callables. Both transform directions are * always present (non-optional) — `definePlugin()` validates both directions * exist before producing this type. * * `customFields` and `mappings` are kept for consumer inspection: `customFields` * shows the specs used to extend the common schema; `mappings` is present when * the author used declarative mappings (absent when hand-written functions were used). */ export interface SchemaWithTransforms { commonSchema: z.ZodType; sourceSchema?: z.ZodType; /** Custom field specs that were used to extend the base schema, kept for inspection. */ customFields?: Record; /** Declarative mappings kept for inspection; absent when hand-written functions were used. */ mappings?: SchemaMappings; toCommon: (source: TSource) => TransformResult; fromCommon: (common: TCommon) => TransformResult; } /** * Plugin identity and capability declaration. * * `name` and `sourceSystem` are required so that plugin registries and * dependency-injection surfaces always have a reliable display label and * provenance string. `version` and `capabilities` remain optional because * they can be inferred or omitted during early development. */ export interface PluginMeta { /** Plugin display name (e.g. `"grants.gov"`). */ name: string; /** Plugin version (semver, e.g. `"1.0.0"`). */ version?: string; /** Name of the source system (e.g. `"grants.gov"`). */ sourceSystem: string; /** Features the plugin provides. */ capabilities?: PluginCapability[]; } /** * Declarative mapping dicts for a single object. * * Each direction is author-provided — `buildTransforms()` does not invert one * direction into the other, because many-to-one handlers like `switch` are not * reversible. */ export interface SchemaMappings { toCommon?: Record; fromCommon?: Record; } export {}; //# sourceMappingURL=types.d.ts.map