Skip to content

Add support for InterfaceTypeDefinition (resolved conflict) #609

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

Merged
merged 17 commits into from
Apr 6, 2024
Merged
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
3 changes: 3 additions & 0 deletions codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ generates:
schema: yup
importFrom: ../types
withObjectType: true
withInterfaceType: true
directives:
required:
msg: required
Expand Down Expand Up @@ -49,6 +50,7 @@ generates:
schema: zod
importFrom: ../types
withObjectType: true
withInterfaceType: true
directives:
# Write directives like
#
Expand All @@ -72,6 +74,7 @@ generates:
schema: myzod
importFrom: ../types
withObjectType: true
withInterfaceType: true
directives:
constraint:
minLength: min
Expand Down
8 changes: 7 additions & 1 deletion example/myzod/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as myzod from 'myzod'
import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, PageInput, PageType, User } from '../types'
import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types'

export const definedNonNullAnySchema = myzod.object({});

Expand Down Expand Up @@ -92,6 +92,12 @@ export function MyTypeFooArgsSchema(): myzod.Type<MyTypeFooArgs> {
})
}

export function NamerSchema(): myzod.Type<Namer> {
return myzod.object({
name: myzod.string().optional().nullable()
})
}

export function PageInputSchema(): myzod.Type<PageInput> {
return myzod.object({
attributes: myzod.array(myzod.lazy(() => AttributeInputSchema())).optional().nullable(),
Expand Down
6 changes: 5 additions & 1 deletion example/test.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Guest {

union UserKind = Admin | Guest

type User {
type User implements Namer {
id: ID
name: String
email: String
Expand All @@ -25,6 +25,10 @@ type User {
updatedAt: Date
}

interface Namer {
name: String
}

input PageInput {
id: ID!
title: String!
Expand Down
6 changes: 5 additions & 1 deletion example/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export type MyTypeFooArgs = {
d: Scalars['Float']['input'];
};

export type Namer = {
name?: Maybe<Scalars['String']['output']>;
};

export type PageInput = {
attributes?: InputMaybe<Array<AttributeInput>>;
date?: InputMaybe<Scalars['Date']['input']>;
Expand All @@ -112,7 +116,7 @@ export enum PageType {
Service = 'SERVICE'
}

export type User = {
export type User = Namer & {
__typename?: 'User';
createdAt?: Maybe<Scalars['Date']['output']>;
email?: Maybe<Scalars['String']['output']>;
Expand Down
8 changes: 7 additions & 1 deletion example/yup/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as yup from 'yup'
import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, PageInput, PageType, User, UserKind } from '../types'
import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User, UserKind } from '../types'

export const ButtonComponentTypeSchema = yup.string<ButtonComponentType>().oneOf(Object.values(ButtonComponentType)).defined();

Expand Down Expand Up @@ -96,6 +96,12 @@ export function MyTypeFooArgsSchema(): yup.ObjectSchema<MyTypeFooArgs> {
})
}

export function NamerSchema(): yup.ObjectSchema<Namer> {
return yup.object({
name: yup.string().defined().nullable().optional()
})
}

export function PageInputSchema(): yup.ObjectSchema<PageInput> {
return yup.object({
attributes: yup.array(yup.lazy(() => AttributeInputSchema().nonNullable())).defined().nullable().optional(),
Expand Down
8 changes: 7 additions & 1 deletion example/zod/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod'
import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, PageInput, PageType, User } from '../types'
import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types'

type Properties<T> = Required<{
[K in keyof T]: z.ZodType<T[K], any, T[K]>;
Expand Down Expand Up @@ -100,6 +100,12 @@ export function MyTypeFooArgsSchema(): z.ZodObject<Properties<MyTypeFooArgs>> {
})
}

export function NamerSchema(): z.ZodObject<Properties<Namer>> {
return z.object({
name: z.string().nullish()
})
}

export function PageInputSchema(): z.ZodObject<Properties<PageInput>> {
return z.object({
attributes: z.array(z.lazy(() => AttributeInputSchema())).nullish(),
Expand Down
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
10 changes: 10 additions & 0 deletions src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
DefinitionNode,
DocumentNode,
GraphQLSchema,
InterfaceTypeDefinitionNode,
ListTypeNode,
NameNode,
NamedTypeNode,
Expand All @@ -23,6 +24,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 function ObjectTypeDefinitionBuilder(useObjectTypes: boolean | undefined, callback: ObjectTypeDefinitionFn): ObjectTypeDefinitionFn | undefined {
if (!useObjectTypes)
Expand All @@ -35,6 +37,14 @@ export function ObjectTypeDefinitionBuilder(useObjectTypes: boolean | undefined,
};
}

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

export function 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 type {
GraphQLSchema,
InputObjectTypeDefinitionNode,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
NameNode,
ObjectTypeDefinitionNode,
TypeNode,
Expand All @@ -18,7 +19,14 @@ import type { ValidationSchemaPluginConfig } from '../config';
import { buildApi, formatDirectiveConfig } from '../directive';
import { BaseSchemaVisitor } from '../schema_visitor';
import type { Visitor } from '../visitor';
import { ObjectTypeDefinitionBuilder, isInput, isListType, isNamedType, isNonNullType } from './../graphql';
import {
InterfaceTypeDefinitionBuilder,
ObjectTypeDefinitionBuilder,
isInput,
isListType,
isNamedType,
isNonNullType,
} from './../graphql';

const anySchema = `definedNonNullAnySchema`;

Expand Down Expand Up @@ -53,6 +61,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 @@ -61,7 +107,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 @@ -266,6 +312,7 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit
const converter = visitor.getNameNodeConverter(node);

switch (converter?.targetKind) {
case 'InterfaceTypeDefinition':
case 'InputObjectTypeDefinition':
case 'ObjectTypeDefinition':
case 'UnionTypeDefinition':
Expand All @@ -279,7 +326,12 @@ function generateNameNodeMyZodSchema(config: ValidationSchemaPluginConfig, visit
}
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 type { FieldDefinitionNode, GraphQLSchema, InputValueDefinitionNode, ObjectTypeDefinitionNode } from 'graphql';
import type {
FieldDefinitionNode,
GraphQLSchema,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
ObjectTypeDefinitionNode,
} from 'graphql';

import type { ValidationSchemaPluginConfig } from './config';
import type { 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
20 changes: 16 additions & 4 deletions src/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { TsVisitor } from '@graphql-codegen/typescript';
import type { FieldDefinitionNode, GraphQLSchema, NameNode, ObjectTypeDefinitionNode } from 'graphql';
import { specifiedScalarTypes } from 'graphql';
import type {
FieldDefinitionNode,
GraphQLSchema,
InterfaceTypeDefinitionNode,
NameNode,
ObjectTypeDefinitionNode,
} from 'graphql';
import {
specifiedScalarTypes,
} from 'graphql';

import type { ValidationSchemaPluginConfig } from './config';

Expand Down Expand Up @@ -37,7 +45,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 @@ -53,7 +65,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
Loading
Loading