diff --git a/example/myzod/schemas.ts b/example/myzod/schemas.ts index 316c00d2..1b4cb08e 100644 --- a/example/myzod/schemas.ts +++ b/example/myzod/schemas.ts @@ -1,5 +1,5 @@ import * as myzod from 'myzod' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, PageInput, PageType, User } from '../types' +import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, PageInput, PageType, User } from '../types' export const definedNonNullAnySchema = myzod.object({}); @@ -76,6 +76,22 @@ export function LayoutInputSchema(): myzod.Type { }) } +export function MyTypeSchema(): myzod.Type { + return myzod.object({ + __typename: myzod.literal('MyType').optional(), + foo: myzod.string().optional().nullable() + }) +} + +export function MyTypeFooArgsSchema(): myzod.Type { + return myzod.object({ + a: myzod.string().optional().nullable(), + b: myzod.number(), + c: myzod.boolean().optional().nullable(), + d: myzod.number() + }) +} + export function PageInputSchema(): myzod.Type { return myzod.object({ attributes: myzod.array(myzod.lazy(() => AttributeInputSchema())).optional().nullable(), diff --git a/example/test.graphql b/example/test.graphql index 62450491..e314b59a 100644 --- a/example/test.graphql +++ b/example/test.graphql @@ -94,6 +94,10 @@ enum HTTPMethod { scalar Date scalar URL +type MyType { + foo(a: String, b: Int!, c: Boolean, d: Float!): String +} + # https://github.com/confuser/graphql-constraint-directive directive @constraint( # String constraints diff --git a/example/types.ts b/example/types.ts index f8f3f610..17afc628 100644 --- a/example/types.ts +++ b/example/types.ts @@ -78,6 +78,19 @@ export type LayoutInput = { dropdown?: InputMaybe; }; +export type MyType = { + __typename?: 'MyType'; + foo?: Maybe; +}; + + +export type MyTypeFooArgs = { + a?: InputMaybe; + b: Scalars['Int']['input']; + c?: InputMaybe; + d: Scalars['Float']['input']; +}; + export type PageInput = { attributes?: InputMaybe>; date?: InputMaybe; diff --git a/example/yup/schemas.ts b/example/yup/schemas.ts index 9d6671d6..3e86db71 100644 --- a/example/yup/schemas.ts +++ b/example/yup/schemas.ts @@ -1,5 +1,5 @@ import * as yup from 'yup' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, PageInput, PageType, User, UserKind } from '../types' +import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, PageInput, PageType, User, UserKind } from '../types' export const ButtonComponentTypeSchema = yup.string().oneOf([ButtonComponentType.Button, ButtonComponentType.Submit]).defined(); @@ -80,6 +80,22 @@ export function LayoutInputSchema(): yup.ObjectSchema { }) } +export function MyTypeSchema(): yup.ObjectSchema { + return yup.object({ + __typename: yup.string<'MyType'>().optional(), + foo: yup.string().defined().nullable().optional() + }) +} + +export function MyTypeFooArgsSchema(): yup.ObjectSchema { + return yup.object({ + a: yup.string().defined().nullable(), + b: yup.number().defined().nonNullable(), + c: yup.boolean().defined().nullable(), + d: yup.number().defined().nonNullable() + }) +} + export function PageInputSchema(): yup.ObjectSchema { return yup.object({ attributes: yup.array(yup.lazy(() => AttributeInputSchema().nonNullable())).defined().nullable().optional(), diff --git a/example/zod/schemas.ts b/example/zod/schemas.ts index 3f03fb08..b6fb5679 100644 --- a/example/zod/schemas.ts +++ b/example/zod/schemas.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, PageInput, PageType, User } from '../types' +import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, PageInput, PageType, User } from '../types' type Properties = Required<{ [K in keyof T]: z.ZodType; @@ -84,6 +84,22 @@ export function LayoutInputSchema(): z.ZodObject> { }) } +export function MyTypeSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('MyType').optional(), + foo: z.string().nullish() + }) +} + +export function MyTypeFooArgsSchema(): z.ZodObject> { + return z.object({ + a: z.string().nullish(), + b: z.number(), + c: z.boolean().nullish(), + d: z.number() + }) +} + export function PageInputSchema(): z.ZodObject> { return z.object({ attributes: z.array(z.lazy(() => AttributeInputSchema())).nullish(), diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 6b47c454..c4f49607 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -73,37 +73,66 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche const name = visitor.convertName(node.name.value); importTypes.push(name); + // Building schema for field arguments. + const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { + importTypes.push(typeName); + const args = field.arguments ?? []; + const shape = args.map(field => generateFieldMyZodSchema(config, visitor, field, 2)).join(',\n'); + switch (config.validationSchemaExportType) { + case 'const': + return new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${typeName}Schema: myzod.Type<${typeName}>`) + .withContent([`myzod.object({`, shape, '})'].join('\n')).string; + + case 'function': + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${typeName}Schema(): myzod.Type<${typeName}>`) + .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')).string; + } + }); + const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; + + // Building schema for fields. const shape = node.fields?.map(field => generateFieldMyZodSchema(config, visitor, field, 2)).join(',\n'); switch (config.validationSchemaExportType) { case 'const': - return new DeclarationBlock({}) - .export() - .asKind('const') - .withName(`${name}Schema: myzod.Type<${name}>`) - .withContent( - [ - `myzod.object({`, - indent(`__typename: myzod.literal('${node.name.value}').optional(),`, 2), - shape, - '})', - ].join('\n') - ).string; + return ( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: myzod.Type<${name}>`) + .withContent( + [ + `myzod.object({`, + indent(`__typename: myzod.literal('${node.name.value}').optional(),`, 2), + shape, + '})', + ].join('\n') + ).string + appendArguments + ); case 'function': default: - return new DeclarationBlock({}) - .export() - .asKind('function') - .withName(`${name}Schema(): myzod.Type<${name}>`) - .withBlock( - [ - indent(`return myzod.object({`), - indent(`__typename: myzod.literal('${node.name.value}').optional(),`, 2), - shape, - indent('})'), - ].join('\n') - ).string; + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): myzod.Type<${name}>`) + .withBlock( + [ + indent(`return myzod.object({`), + indent(`__typename: myzod.literal('${node.name.value}').optional(),`, 2), + shape, + indent('})'), + ].join('\n') + ).string + appendArguments + ); } }), }, diff --git a/src/visitor.ts b/src/visitor.ts index d95b1892..cc06e829 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -1,5 +1,5 @@ import { TsVisitor } from '@graphql-codegen/typescript'; -import { GraphQLSchema, NameNode, specifiedScalarTypes } from 'graphql'; +import { FieldDefinitionNode, GraphQLSchema, NameNode, ObjectTypeDefinitionNode, specifiedScalarTypes } from 'graphql'; import { ValidationSchemaPluginConfig } from './config'; @@ -50,4 +50,28 @@ export class Visitor extends TsVisitor { const tsType = this.getScalarType(name); return tsType === 'string'; } + + public buildArgumentsSchemaBlock( + node: ObjectTypeDefinitionNode, + callback: (typeName: string, field: FieldDefinitionNode) => string + ) { + const fieldsWithArguments = node.fields?.filter(field => field.arguments && field.arguments.length > 0) ?? []; + if (fieldsWithArguments.length === 0) { + return undefined; + } + return fieldsWithArguments + .map(field => { + const name = + node.name.value + + (this.config.addUnderscoreToArgsType ? '_' : '') + + this.convertName(field, { + useTypesPrefix: false, + useTypesSuffix: false, + }) + + 'Args'; + + return callback(name, field); + }) + .join('\n'); + } } diff --git a/src/yup/index.ts b/src/yup/index.ts index 4aeb38b8..96ac7dc4 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -88,6 +88,32 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema const name = visitor.convertName(node.name.value); importTypes.push(name); + // Building schema for field arguments. + const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { + importTypes.push(typeName); + const args = field.arguments ?? []; + const shape = args.map(field => generateFieldYupSchema(config, visitor, field, 2)).join(',\n'); + switch (config.validationSchemaExportType) { + case 'const': + return new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${typeName}Schema: yup.ObjectSchema<${typeName}>`) + .withContent([`yup.object({`, shape, '})'].join('\n')).string; + + case 'function': + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${typeName}Schema(): yup.ObjectSchema<${typeName}>`) + .withBlock([indent(`return yup.object({`), shape, indent('})')].join('\n')).string; + } + }); + const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; + + // Building schema for fields. + const shape = node.fields ?.map(field => { const fieldSchema = generateFieldYupSchema(config, visitor, field, 2); @@ -97,33 +123,37 @@ export const YupSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema switch (config.validationSchemaExportType) { case 'const': - return new DeclarationBlock({}) - .export() - .asKind('const') - .withName(`${name}Schema: yup.ObjectSchema<${name}>`) - .withContent( - [ - `yup.object({`, - indent(`__typename: yup.string<'${node.name.value}'>().optional(),`, 2), - shape, - '})', - ].join('\n') - ).string; + return ( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: yup.ObjectSchema<${name}>`) + .withContent( + [ + `yup.object({`, + indent(`__typename: yup.string<'${node.name.value}'>().optional(),`, 2), + shape, + '})', + ].join('\n') + ).string + appendArguments + ); case 'function': default: - return new DeclarationBlock({}) - .export() - .asKind('function') - .withName(`${name}Schema(): yup.ObjectSchema<${name}>`) - .withBlock( - [ - indent(`return yup.object({`), - indent(`__typename: yup.string<'${node.name.value}'>().optional(),`, 2), - shape, - indent('})'), - ].join('\n') - ).string; + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): yup.ObjectSchema<${name}>`) + .withBlock( + [ + indent(`return yup.object({`), + indent(`__typename: yup.string<'${node.name.value}'>().optional(),`, 2), + shape, + indent('})'), + ].join('\n') + ).string + appendArguments + ); } }), }, diff --git a/src/zod/index.ts b/src/zod/index.ts index 3d01931c..bc457544 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -89,34 +89,66 @@ export const ZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchema const name = visitor.convertName(node.name.value); importTypes.push(name); + // Building schema for field arguments. + const argumentBlocks = visitor.buildArgumentsSchemaBlock(node, (typeName, field) => { + importTypes.push(typeName); + const args = field.arguments ?? []; + const shape = args.map(field => generateFieldZodSchema(config, visitor, field, 2)).join(',\n'); + switch (config.validationSchemaExportType) { + case 'const': + return new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${typeName}Schema: z.ZodObject>`) + .withContent([`z.object({`, shape, '})'].join('\n')).string; + + case 'function': + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${typeName}Schema(): z.ZodObject>`) + .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')).string; + } + }); + const appendArguments = argumentBlocks ? '\n' + argumentBlocks : ''; + + // Building schema for fields. const shape = node.fields?.map(field => generateFieldZodSchema(config, visitor, field, 2)).join(',\n'); switch (config.validationSchemaExportType) { case 'const': - return new DeclarationBlock({}) - .export() - .asKind('const') - .withName(`${name}Schema: z.ZodObject>`) - .withContent( - [`z.object({`, indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), shape, '})'].join( - '\n' - ) - ).string; + return ( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${name}Schema: z.ZodObject>`) + .withContent( + [ + `z.object({`, + indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), + shape, + '})', + ].join('\n') + ).string + appendArguments + ); case 'function': default: - return new DeclarationBlock({}) - .export() - .asKind('function') - .withName(`${name}Schema(): z.ZodObject>`) - .withBlock( - [ - indent(`return z.object({`), - indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), - shape, - indent('})'), - ].join('\n') - ).string; + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): z.ZodObject>`) + .withBlock( + [ + indent(`return z.object({`), + indent(`__typename: z.literal('${node.name.value}').optional(),`, 2), + shape, + indent('})'), + ].join('\n') + ).string + appendArguments + ); } }), }, diff --git a/tests/myzod.spec.ts b/tests/myzod.spec.ts index 3c9826d8..c0b91ecb 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -803,6 +803,38 @@ describe('myzod', () => { expect(result.content).toContain(wantContain); } }); + + it('with object arguments', async () => { + const schema = buildSchema(/* GraphQL */ ` + type MyType { + foo(a: String, b: Int!, c: Boolean, d: Float!, e: Text): String + } + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + withObjectType: true, + scalars: { + Text: 'string', + }, + }, + {} + ); + const wantContain = dedent` + export function MyTypeFooArgsSchema(): myzod.Type { + return myzod.object({ + a: myzod.string().optional().nullable(), + b: myzod.number(), + c: myzod.boolean().optional().nullable(), + d: myzod.number(), + e: myzod.string().optional().nullable() + }) + }`; + expect(result.content).toContain(wantContain); + }); }); it('properly generates custom directive values', async () => { diff --git a/tests/yup.spec.ts b/tests/yup.spec.ts index 5112f8a2..86ae3d6f 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -717,6 +717,38 @@ describe('yup', () => { expect(result.content).toContain(wantContain); } }); + + it('with object arguments', async () => { + const schema = buildSchema(/* GraphQL */ ` + type MyType { + foo(a: String, b: Int!, c: Boolean, d: Float!, e: Text): String + } + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'yup', + withObjectType: true, + scalars: { + Text: 'string', + }, + }, + {} + ); + const wantContain = dedent` + export function MyTypeFooArgsSchema(): yup.ObjectSchema { + return yup.object({ + a: yup.string().defined().nullable(), + b: yup.number().defined().nonNullable(), + c: yup.boolean().defined().nullable(), + d: yup.number().defined().nonNullable(), + e: yup.string().defined().nullable() + }) + }`; + expect(result.content).toContain(wantContain); + }); }); it('properly generates custom directive values', async () => { diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index 67a676c2..76f3ecde 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -871,6 +871,38 @@ describe('zod', () => { expect(result.content).toContain(wantContain); } }); + + it('with object arguments', async () => { + const schema = buildSchema(/* GraphQL */ ` + type MyType { + foo(a: String, b: Int!, c: Boolean, d: Float!, e: Text): String + } + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + withObjectType: true, + scalars: { + Text: 'string', + }, + }, + {} + ); + const wantContain = dedent` + export function MyTypeFooArgsSchema(): z.ZodObject> { + return z.object({ + a: z.string().nullish(), + b: z.number(), + c: z.boolean().nullish(), + d: z.number(), + e: z.string().nullish() + }) + }`; + expect(result.content).toContain(wantContain); + }); }); it('properly generates custom directive values', async () => {