/** * Result Schema Generator * Generates Zod schemas for Prisma operation return values to enable validation of API responses and operation results */ import { DMMF } from '@prisma/generator-helper'; import { GeneratorConfig } from '../config/parser'; import type { CustomImport } from '../parsers/zod-comments'; /** * Prisma operation types that return results */ export declare enum OperationType { FIND_UNIQUE = "findUnique", FIND_FIRST = "findFirst", FIND_MANY = "findMany", CREATE = "create", CREATE_MANY = "createMany", UPDATE = "update", UPDATE_MANY = "updateMany", UPSERT = "upsert", DELETE = "delete", DELETE_MANY = "deleteMany", AGGREGATE = "aggregate", GROUP_BY = "groupBy", COUNT = "count" } /** * Result schema generation options */ export interface ResultSchemaOptions { modelName: string; operationType: OperationType; includeRelations?: string[]; excludeFields?: string[]; paginationSupport?: boolean; nullableResult?: boolean; customValidations?: Record; } /** * Pagination schema configuration */ export interface PaginationConfig { includeCursor?: boolean; includeCount?: boolean; includePageInfo?: boolean; customFields?: Record; } /** * Aggregate operation configuration */ export interface AggregateConfig { includeCount?: boolean; includeSum?: string[]; includeAvg?: string[]; includeMin?: string[]; includeMax?: string[]; customAggregates?: Record; } /** * Reference to a pure model schema that a result schema depends on. * `exportName` is the exported identifier (e.g. `TagSchema`) and `fileBase` * is the model file name without its `.ts` extension (e.g. `Tag.schema`). */ export interface ResultModelSchemaRef { exportName: string; fileBase: string; } /** * Where the emitted result files sit and how they import, for a relative * `typedJson.schemaModule`. Supplied by the transformer, which owns the output layout; * omit it and a relative specifier is emitted exactly as configured, which is what a * caller driving this generator directly wants and what `typedJson.map` needs nothing * of, since a mapped expression imports nothing at all. */ export interface ResultTypedJsonContext { /** Directory of the emitted file relative to the output root, e.g. `schemas/results`. */ outputSubdir?: string; /** Extension to append to relative specifiers, e.g. `.js` under NodeNext. */ importExtension?: string; /** True in single-file mode, where imports must be one name per statement. */ singleFile?: boolean; } /** * Generated result schema information */ export interface GeneratedResultSchema { operationType: OperationType; schemaName: string; zodSchema: string; typeDefinition: string; imports: Set; exports: Set; dependencies: string[]; /** * Pure model schemas (by Prisma model name) referenced by relation fields in * this result schema. The transformer turns these into `../models/*` imports. * Separate from `dependencies` (which routes through `../objects`). */ modelDependencies: string[]; /** * Imports the typed-JSON replacements in this schema need, already merged and * filtered to the names its body mentions. Empty unless * `typedJson.applyToResults` is on. */ typedJsonImports: CustomImport[]; documentation: string; examples?: string[]; } /** * Result schema generation context */ export interface ResultGenerationContext { model: DMMF.Model; options: ResultSchemaOptions; baseModelSchema?: string; } /** * Result Schema Generator * Main class for generating Zod schemas for Prisma operation results */ export declare class ResultSchemaGenerator { private generatedSchemas; private baseModelSchemas; private config; /** * Pure model schemas that are known to be emitted and safe to reference from * result schemas, keyed by Prisma model name. Populated by the transformer via * {@link setAvailablePureModels}. When a relation's target model is present * here, the relation field references its `Schema`; otherwise it falls * back to `z.unknown()`. Empty in single-file mode (references cannot resolve * across the inlined bundle) so relations degrade to the safe fallback. */ private availablePureModels; /** Where the emitted files sit, for rewriting a relative `typedJson.schemaModule`. */ private typedJsonContext; private isJsonSchemaModeEnabled; /** * Zod expression for a Decimal field in a result schema, honoring decimalMode. * * Result schemas validate what Prisma *returns*, and in the default 'decimal' * mode that is a Prisma.Decimal instance — so a plain z.number() would reject * real query results. The 'decimal' expression is deliberately import-free * (structural check on the decimal.js shape) so result files stay * dependency-free and keep working in single-file bundles; it also matches * Decimal instances coming from a different runtime copy of the class. * * It remains a union with number and numeric string so that callers already * parsing serialized results (which previously validated against z.number()) * keep working — the change only adds the Decimal shape that used to be * rejected. */ private decimalResultExpression; private getJsonSchemaOptions; constructor(config?: GeneratorConfig, typedJsonContext?: ResultTypedJsonContext); /** * The `typedJson` block, but only when it opted results in. * * `null` is the answer for an unconfigured generate *and* for the default * `applyToResults: false`, and it means "emit exactly what 3.0.0 emitted". A result * schema describes what the database already returned: narrowing it makes a row * written before the annotation existed fail on READ, so the read path is opt-in * even though the write path is not. */ private typedJsonConfig; /** * The Zod expression a PJTG annotation gives this field, or `null` to map it exactly * as before. Returns the ELEMENT expression for a list field, because every caller * applies its own `z.array(...)` wrapper afterwards. */ private typedJsonExpression; /** * Imports for the replacements this body actually mentions. * * Resolved from the model's fields rather than accumulated while building, because one * annotated field reaches several slots of the same file (`_min`, `_max`, the grouped * value) and only the finished body knows which names survived into it. Filtering on * the body is what keeps `WorkflowAggregateResult` - which narrows min/max but never * touches a Json column - from importing a schema it never names. */ private collectTypedJsonImports; /** * Register the set of pure model schemas that are emitted and safe to * reference from generated result schemas. Called by the transformer, which * owns knowledge of model enablement, the pure-model emission predicate and * the pure-model naming resolver. Passing an empty map (the single-file case) * makes every relation field fall back to `z.unknown()`. */ setAvailablePureModels(models: Map): void; /** * Generate result schema for a specific operation */ generateResultSchema(model: DMMF.Model, options: ResultSchemaOptions): GeneratedResultSchema; /** * Generate schemas for all operations of a model */ generateAllResultSchemas(model: DMMF.Model, operationTypes?: OperationType[]): GeneratedResultSchema[]; /** * Build generation context */ private buildGenerationContext; /** * Generate single result schema (for operations returning one model or null) */ private generateSingleResultSchema; /** * Generate array result schema (for findMany operations) */ private generateArrayResultSchema; /** * Generate batch operation result schema */ private generateBatchResultSchema; /** * Generate aggregate result schema */ private generateAggregateResultSchema; /** * Generate groupBy result schema */ private generateGroupByResultSchema; /** * Generate count result schema */ private generateCountResultSchema; /** * Helper methods */ private generateSchemaName; private operationTypeToSuffix; private isNullableOperation; private buildBaseResultSchema; /** * Build the Zod expression for a relation (object) field in a record-shaped * result schema. References the related model's pure schema when it is emitted * and safe to import; otherwise falls back to `z.unknown()`. Always optional * because relations are only present when the query `include`s them. * * Self-relations (target model === current model) use the fallback so the * result file never has to import a schema for a cyclic/self reference, * mirroring the existing guard in {@link extractDependencies}. */ private buildRelationFieldSchema; private buildRelationSchema; /** * The `_count` / `_sum` / `_avg` / `_min` / `_max` slots of an aggregate result. * * Membership and optionality both follow Prisma, measured from the DMMF for a model * carrying every scalar type, a list of each, an enum and a relation: * * Count -> every scalar including lists, no relations, plus `_all` * Min -> non-list scalars and enums, plus Boolean, Bytes and DateTime; no Json * Max -> same as Min * Sum -> numeric columns including lists, and a list column sums to an ARRAY * Avg -> numeric columns including lists, always a single number (Decimal stays Decimal) * * Every member is `.optional()`, which is the part that was actually breaking reads. * An aggregate selects its members: `aggregate({ _count: { id: true } })` returns * `_count: { id: 3 }` and nothing else, so a required member rejects the real response. * Emitting relation counts and list columns in `_min` made that certain rather than * merely likely, since Prisma can never supply them. */ private buildAggregateFields; /** * The grouped columns of a `groupBy` row, before the aggregate slots. * * Every column is optional, because a groupBy returns the columns named in `by` and * nothing else: `groupBy({ by: ['name'] })` gives `[{ name: 'x' }]`. Prisma's * `GroupByOutputType` lists them all because it is the maximal type, and the client * narrows it per call with `Pick<..., T['by'][number]>`. A runtime schema cannot see * `by`, so requiring them rejected nearly every real response. * * A nullable column also gets `.nullable()`, since null is its ordinary value and * `.optional()` admits only undefined. * * Enums are groupable and appear in Prisma's own output type, so the old * `kind === 'scalar'` filter simply lost those columns. Arrays are groupable on * PostgreSQL and stay. */ private buildGroupByFields; /** * What a non-required column's schema needs appended, given the expression it already got. * * A nullable column's ordinary value is `null`: that is what Prisma returns for it, and * a groupBy that does not name the column in `by` leaves it out altogether. Both were * already true of the untyped expressions, which is why nobody noticed - `z.unknown()` * accepts null and undefined on its own, so the missing `.nullable()` never showed. A * replacement schema accepts neither, so this is where the flag either works on ordinary * data or does not work at all. * * Gated on `typed` for one reason: correcting the untyped path would change the emitted * bytes of every tree that has no `typedJson` block at all (`z.string().optional()` would * become nullish, and a groupBy field would gain markers it has never had). That is the * one thing the flag-off contract forbids, so the untyped defect stays put and is pinned * by a test instead. With no `typedJson.applyToResults`, `typed` is never true and this * method returns `fallback`, which is the marker the call site emitted before. */ private optionalityMarkers; private generatePaginationSchema; /** * The expression alone, for the `_min` / `_max` slots, which append their own * `.nullable()` whatever produced it and so have nothing to ask. */ private mapPrismaTypeToZod; /** * The Zod expression for one field of the model, in every result schema that carries * it - the record-shaped results, the grouped values, and the `_min` / `_max` slots - * and whether a PJTG annotation is what produced it. * * `model` is required rather than optional so that a new call site cannot quietly opt * out of the annotation lookup and emit a different schema for the same column than * the file next to it. * * `typed` is here because the two callers that append optionality need it: what a * nullable column has to accept is the same either way, and only a replacement fails to * accept it unaided. See {@link optionalityMarkers}. */ private mapPrismaFieldToZod; /** * The expression for a field no annotation reaches: the 3.0.0 type map, unchanged, and * the only expression a repo with no `typedJson` block ever sees. */ private mapUntypedPrismaTypeToZod; private getBaseModelSchema; private extractDependencies; private generateDocumentation; private generateExamples; private generateCacheKey; } export default ResultSchemaGenerator;