Skip to content

Add support for InterfaceTypeDefinition #530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
18 changes: 18 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,24 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig {
* ```
*/
withObjectType?: boolean;
/**
* @description Generates validation schema with GraphQL type interfaces.
*
* @exampleMarkdown
* ```yml
* generates:
* path/to/types.ts:
* plugins:
* - typescript
* path/to/schemas.ts:
* plugins:
* - graphql-codegen-validation-schema
* config:
* schema: yup
* withInterfaceType: true
* ```
*/
withInterfaceType?: boolean;
/**
* @description Specify validation schema export type.
* @default function
Expand Down
12 changes: 12 additions & 0 deletions src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DefinitionNode,
DocumentNode,
GraphQLSchema,
InterfaceTypeDefinitionNode,
isSpecifiedScalarType,
ListTypeNode,
NamedTypeNode,
Expand All @@ -21,6 +22,7 @@ export const isNamedType = (typ?: TypeNode): typ is NamedTypeNode => typ?.kind =
export const isInput = (kind: string) => kind.includes('Input');

type ObjectTypeDefinitionFn = (node: ObjectTypeDefinitionNode) => any;
type InterfaceTypeDefinitionFn = (node: InterfaceTypeDefinitionNode) => any;

export const ObjectTypeDefinitionBuilder = (
useObjectTypes: boolean | undefined,
Expand All @@ -35,6 +37,16 @@ export const ObjectTypeDefinitionBuilder = (
};
};

export const InterfaceTypeDefinitionBuilder = (
useInterfaceTypes: boolean | undefined,
callback: InterfaceTypeDefinitionFn
): InterfaceTypeDefinitionFn | undefined => {
if (!useInterfaceTypes) return undefined;
return node => {
return callback(node);
};
};

export const topologicalSortAST = (schema: GraphQLSchema, ast: DocumentNode): DocumentNode => {
const dependencyGraph = new Graph();
const targetKinds = [
Expand Down
56 changes: 54 additions & 2 deletions src/myzod/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
GraphQLSchema,
InputObjectTypeDefinitionNode,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
NameNode,
ObjectTypeDefinitionNode,
TypeNode,
Expand All @@ -15,7 +16,14 @@ import { ValidationSchemaPluginConfig } from '../config';
import { buildApi, formatDirectiveConfig } from '../directive';
import { BaseSchemaVisitor } from '../schema_visitor';
import { Visitor } from '../visitor';
import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql';
import {
InterfaceTypeDefinitionBuilder,
isInput,
isListType,
isNamedType,
isNonNullType,
ObjectTypeDefinitionBuilder,
} from './../graphql';

const anySchema = `definedNonNullAnySchema`;

Expand Down Expand Up @@ -50,6 +58,44 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor {
};
}

get InterfaceTypeDefinition() {
return {
leave: InterfaceTypeDefinitionBuilder(this.config.withInterfaceType, (node: InterfaceTypeDefinitionNode) => {
const visitor = this.createVisitor('output');
const name = visitor.convertName(node.name.value);
this.importTypes.push(name);

// Building schema for field arguments.
const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor);
const appendArguments = argumentBlocks ? '\n' + argumentBlocks : '';

// Building schema for fields.
const shape = node.fields?.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n');

switch (this.config.validationSchemaExportType) {
case 'const':
return (
new DeclarationBlock({})
.export()
.asKind('const')
.withName(`${name}Schema: myzod.Type<${name}>`)
.withContent([`myzod.object({`, 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({`), shape, indent('})')].join('\n')).string + appendArguments
);
}
}),
};
}

get ObjectTypeDefinition() {
return {
leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => {
Expand All @@ -58,7 +104,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor {
this.importTypes.push(name);

// Building schema for field arguments.
const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor);
const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor);
const appendArguments = argumentBlocks ? '\n' + argumentBlocks : '';

// Building schema for fields.
Expand Down Expand Up @@ -270,6 +316,7 @@ const generateNameNodeMyZodSchema = (
const converter = visitor.getNameNodeConverter(node);

switch (converter?.targetKind) {
case 'InterfaceTypeDefinition':
case 'InputObjectTypeDefinition':
case 'ObjectTypeDefinition':
case 'UnionTypeDefinition':
Expand All @@ -283,7 +330,12 @@ const generateNameNodeMyZodSchema = (
}
case 'EnumTypeDefinition':
return `${converter.convertName()}Schema`;
case 'ScalarTypeDefinition':
return myzod4Scalar(config, visitor, node.value);
default:
if (converter?.targetKind) {
console.warn('Unknown target kind', converter.targetKind);
}
return myzod4Scalar(config, visitor, node.value);
}
};
Expand Down
13 changes: 11 additions & 2 deletions src/schema_visitor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { FieldDefinitionNode, GraphQLSchema, InputValueDefinitionNode, ObjectTypeDefinitionNode } from 'graphql';
import {
FieldDefinitionNode,
GraphQLSchema,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
ObjectTypeDefinitionNode,
} from 'graphql';

import { ValidationSchemaPluginConfig } from './config';
import { SchemaVisitor } from './types';
Expand Down Expand Up @@ -39,7 +45,10 @@ export abstract class BaseSchemaVisitor implements SchemaVisitor {
name: string
): string;

protected buildObjectTypeDefinitionArguments(node: ObjectTypeDefinitionNode, visitor: Visitor) {
protected buildTypeDefinitionArguments(
node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode,
visitor: Visitor
) {
return visitor.buildArgumentsSchemaBlock(node, (typeName, field) => {
this.importTypes.push(typeName);
return this.buildInputFields(field.arguments ?? [], visitor, typeName);
Expand Down
17 changes: 14 additions & 3 deletions src/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { TsVisitor } from '@graphql-codegen/typescript';
import { FieldDefinitionNode, GraphQLSchema, NameNode, ObjectTypeDefinitionNode, specifiedScalarTypes } from 'graphql';
import {
FieldDefinitionNode,
GraphQLSchema,
InterfaceTypeDefinitionNode,
NameNode,
ObjectTypeDefinitionNode,
specifiedScalarTypes,
} from 'graphql';

import { ValidationSchemaPluginConfig } from './config';

Expand Down Expand Up @@ -36,7 +43,11 @@ export class Visitor extends TsVisitor {
if (this.scalarDirection === 'both') {
return null;
}
return this.scalars[scalarName][this.scalarDirection];
const scalar = this.scalars[scalarName];
if (!scalar) {
throw new Error(`Unknown scalar ${scalarName}`);
}
return scalar[this.scalarDirection];
}

public shouldEmitAsNotAllowEmptyString(name: string): boolean {
Expand All @@ -52,7 +63,7 @@ export class Visitor extends TsVisitor {
}

public buildArgumentsSchemaBlock(
node: ObjectTypeDefinitionNode,
node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode,
callback: (typeName: string, field: FieldDefinitionNode) => string
) {
const fieldsWithArguments = node.fields?.filter(field => field.arguments && field.arguments.length > 0) ?? [];
Expand Down
56 changes: 54 additions & 2 deletions src/yup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
GraphQLSchema,
InputObjectTypeDefinitionNode,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
NameNode,
ObjectTypeDefinitionNode,
TypeNode,
Expand All @@ -15,7 +16,14 @@ import { ValidationSchemaPluginConfig } from '../config';
import { buildApi, formatDirectiveConfig } from '../directive';
import { BaseSchemaVisitor } from '../schema_visitor';
import { Visitor } from '../visitor';
import { isInput, isListType, isNamedType, isNonNullType, ObjectTypeDefinitionBuilder } from './../graphql';
import {
InterfaceTypeDefinitionBuilder,
isInput,
isListType,
isNamedType,
isNonNullType,
ObjectTypeDefinitionBuilder,
} from './../graphql';

export class YupSchemaVisitor extends BaseSchemaVisitor {
constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) {
Expand Down Expand Up @@ -56,6 +64,49 @@ export class YupSchemaVisitor extends BaseSchemaVisitor {
};
}

get InterfaceTypeDefinition() {
return {
leave: InterfaceTypeDefinitionBuilder(this.config.withInterfaceType, (node: InterfaceTypeDefinitionNode) => {
const visitor = this.createVisitor('output');
const name = visitor.convertName(node.name.value);
this.importTypes.push(name);

// Building schema for field arguments.
const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor);
const appendArguments = argumentBlocks ? '\n' + argumentBlocks : '';

// Building schema for fields.
const shape = node.fields
?.map(field => {
const fieldSchema = generateFieldYupSchema(this.config, visitor, field, 2);
return isNonNullType(field.type) ? fieldSchema : `${fieldSchema}.optional()`;
})
.join(',\n');

switch (this.config.validationSchemaExportType) {
case 'const':
return (
new DeclarationBlock({})
.export()
.asKind('const')
.withName(`${name}Schema: yup.ObjectSchema<${name}>`)
.withContent([`yup.object({`, 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({`), shape, indent('})')].join('\n')).string + appendArguments
);
}
}),
};
}

get ObjectTypeDefinition() {
return {
leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => {
Expand All @@ -64,7 +115,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor {
this.importTypes.push(name);

// Building schema for field arguments.
const argumentBlocks = this.buildObjectTypeDefinitionArguments(node, visitor);
const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor);
const appendArguments = argumentBlocks ? '\n' + argumentBlocks : '';

// Building schema for fields.
Expand Down Expand Up @@ -282,6 +333,7 @@ const generateNameNodeYupSchema = (config: ValidationSchemaPluginConfig, visitor
const converter = visitor.getNameNodeConverter(node);

switch (converter?.targetKind) {
case 'InterfaceTypeDefinition':
case 'InputObjectTypeDefinition':
case 'ObjectTypeDefinition':
case 'UnionTypeDefinition':
Expand Down
Loading