Skip to content
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

fix 373 #375

Merged
merged 5 commits into from
May 27, 2023
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ generates:
# You can put the config for typescript plugin here
# see: https://www.graphql-code-generator.com/plugins/typescript
strictScalars: true
# Overrides built-in ID scalar to both input and output types as string.
# see: https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#scalars
scalars:
ID: string
# You can also write the config for this plugin together
schema: yup # or zod
```

It is recommended to write `scalars` config for built-in type `ID`, as in the yaml example shown above. For more information: [#375](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/pull/375)

You can check [example](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/tree/main/example) directory if you want to see more complex config example or how is generated some files.

The Q&A for each schema is written in the README in the respective example directory.
Expand Down
9 changes: 9 additions & 0 deletions codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ generates:
example/types.ts:
plugins:
- typescript
config:
scalars:
ID: string
example/yup/schemas.ts:
plugins:
- ./dist/main/index.js:
Expand Down Expand Up @@ -38,6 +41,8 @@ generates:
max: ['max', '$1 + 1']
exclusiveMin: min
exclusiveMax: max
scalars:
ID: string
example/zod/schemas.ts:
plugins:
- ./dist/main/index.js:
Expand All @@ -59,6 +64,8 @@ generates:
startsWith: ['regex', '/^$1/', 'message']
format:
email: email
scalars:
ID: string
example/myzod/schemas.ts:
plugins:
- ./dist/main/index.js:
Expand All @@ -72,3 +79,5 @@ generates:
startsWith: ['pattern', '/^$1/']
format:
email: email
scalars:
ID: string
60 changes: 31 additions & 29 deletions example/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,27 @@ export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
Date: any;
URL: any;
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
Date: { input: any; output: any; }
URL: { input: any; output: any; }
};

export type Admin = {
__typename?: 'Admin';
lastModifiedAt?: Maybe<Scalars['Date']>;
lastModifiedAt?: Maybe<Scalars['Date']['output']>;
};

export type AttributeInput = {
key?: InputMaybe<Scalars['String']>;
val?: InputMaybe<Scalars['String']>;
key?: InputMaybe<Scalars['String']['input']>;
val?: InputMaybe<Scalars['String']['input']>;
};

export enum ButtonComponentType {
Expand All @@ -33,7 +35,7 @@ export type ComponentInput = {
child?: InputMaybe<ComponentInput>;
childrens?: InputMaybe<Array<InputMaybe<ComponentInput>>>;
event?: InputMaybe<EventInput>;
name: Scalars['String'];
name: Scalars['String']['input'];
type: ButtonComponentType;
};

Expand All @@ -43,8 +45,8 @@ export type DropDownComponentInput = {
};

export type EventArgumentInput = {
name: Scalars['String'];
value: Scalars['String'];
name: Scalars['String']['input'];
value: Scalars['String']['input'];
};

export type EventInput = {
Expand All @@ -59,12 +61,12 @@ export enum EventOptionType {

export type Guest = {
__typename?: 'Guest';
lastLoggedIn?: Maybe<Scalars['Date']>;
lastLoggedIn?: Maybe<Scalars['Date']['output']>;
};

export type HttpInput = {
method?: InputMaybe<HttpMethod>;
url: Scalars['URL'];
url: Scalars['URL']['input'];
};

export enum HttpMethod {
Expand All @@ -78,16 +80,16 @@ export type LayoutInput = {

export type PageInput = {
attributes?: InputMaybe<Array<AttributeInput>>;
date?: InputMaybe<Scalars['Date']>;
height: Scalars['Float'];
id: Scalars['ID'];
date?: InputMaybe<Scalars['Date']['input']>;
height: Scalars['Float']['input'];
id: Scalars['ID']['input'];
layout: LayoutInput;
pageType: PageType;
postIDs?: InputMaybe<Array<Scalars['ID']>>;
show: Scalars['Boolean'];
tags?: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
title: Scalars['String'];
width: Scalars['Int'];
postIDs?: InputMaybe<Array<Scalars['ID']['input']>>;
show: Scalars['Boolean']['input'];
tags?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
title: Scalars['String']['input'];
width: Scalars['Int']['input'];
};

export enum PageType {
Expand All @@ -99,13 +101,13 @@ export enum PageType {

export type User = {
__typename?: 'User';
createdAt?: Maybe<Scalars['Date']>;
email?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['ID']>;
createdAt?: Maybe<Scalars['Date']['output']>;
email?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
kind?: Maybe<UserKind>;
name?: Maybe<Scalars['String']>;
password?: Maybe<Scalars['String']>;
updatedAt?: Maybe<Scalars['Date']>;
name?: Maybe<Scalars['String']['output']>;
password?: Maybe<Scalars['String']['output']>;
updatedAt?: Maybe<Scalars['Date']['output']>;
};

export type UserKind = Admin | Guest;
80 changes: 38 additions & 42 deletions src/myzod/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ import {
UnionTypeDefinitionNode,
} from 'graphql';
import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common';
import { TsVisitor } from '@graphql-codegen/typescript';
import { Visitor } from '../visitor';
import { buildApi, formatDirectiveConfig } from '../directive';
import { SchemaVisitor } from '../types';

const importZod = `import * as myzod from 'myzod'`;
const anySchema = `definedNonNullAnySchema`;

export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSchemaPluginConfig): SchemaVisitor => {
const tsVisitor = new TsVisitor(schema, config);

const importTypes: string[] = [];

return {
Expand All @@ -39,12 +37,11 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche
].join('\n'),
InputObjectTypeDefinition: {
leave: (node: InputObjectTypeDefinitionNode) => {
const name = tsVisitor.convertName(node.name.value);
const visitor = new Visitor('input', schema, config);
const name = visitor.convertName(node.name.value);
importTypes.push(name);

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

return new DeclarationBlock({})
.export()
Expand All @@ -55,12 +52,11 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche
},
ObjectTypeDefinition: {
leave: ObjectTypeDefinitionBuilder(config.withObjectType, (node: ObjectTypeDefinitionNode) => {
const name = tsVisitor.convertName(node.name.value);
const visitor = new Visitor('output', schema, config);
const name = visitor.convertName(node.name.value);
importTypes.push(name);

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

return new DeclarationBlock({})
.export()
Expand All @@ -78,7 +74,8 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche
},
EnumTypeDefinition: {
leave: (node: EnumTypeDefinitionNode) => {
const enumname = tsVisitor.convertName(node.name.value);
const visitor = new Visitor('both', schema, config);
const enumname = visitor.convertName(node.name.value);
importTypes.push(enumname);
// z.enum are basically myzod.literals
if (config.enumsAsTypes) {
Expand All @@ -101,11 +98,13 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche
leave: (node: UnionTypeDefinitionNode) => {
if (!node.types || !config.withObjectType) return;

const unionName = tsVisitor.convertName(node.name.value);
const visitor = new Visitor('output', schema, config);

const unionName = visitor.convertName(node.name.value);
const unionElements = node.types
?.map(t => {
const element = tsVisitor.convertName(t.name.value);
const typ = schema.getType(t.name.value);
const element = visitor.convertName(t.name.value);
const typ = visitor.getType(t.name.value);
if (typ?.astNode?.kind === 'EnumTypeDefinition') {
return `${element}Schema`;
}
Expand All @@ -126,25 +125,23 @@ export const MyZodSchemaVisitor = (schema: GraphQLSchema, config: ValidationSche

const generateFieldMyZodSchema = (
config: ValidationSchemaPluginConfig,
tsVisitor: TsVisitor,
schema: GraphQLSchema,
visitor: Visitor,
field: InputValueDefinitionNode | FieldDefinitionNode,
indentCount: number
): string => {
const gen = generateFieldTypeMyZodSchema(config, tsVisitor, schema, field, field.type);
const gen = generateFieldTypeMyZodSchema(config, visitor, field, field.type);
return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount);
};

const generateFieldTypeMyZodSchema = (
config: ValidationSchemaPluginConfig,
tsVisitor: TsVisitor,
schema: GraphQLSchema,
visitor: Visitor,
field: InputValueDefinitionNode | FieldDefinitionNode,
type: TypeNode,
parentType?: TypeNode
): string => {
if (isListType(type)) {
const gen = generateFieldTypeMyZodSchema(config, tsVisitor, schema, field, type.type, type);
const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type);
if (!isNonNullType(parentType)) {
const arrayGen = `myzod.array(${maybeLazy(type.type, gen)})`;
const maybeLazyGen = applyDirectives(config, field, arrayGen);
Expand All @@ -153,18 +150,18 @@ const generateFieldTypeMyZodSchema = (
return `myzod.array(${maybeLazy(type.type, gen)})`;
}
if (isNonNullType(type)) {
const gen = generateFieldTypeMyZodSchema(config, tsVisitor, schema, field, type.type, type);
const gen = generateFieldTypeMyZodSchema(config, visitor, field, type.type, type);
return maybeLazy(type.type, gen);
}
if (isNamedType(type)) {
const gen = generateNameNodeMyZodSchema(config, tsVisitor, schema, type.name);
const gen = generateNameNodeMyZodSchema(config, visitor, type.name);
if (isListType(parentType)) {
return `${gen}.nullable()`;
}
const appliedDirectivesGen = applyDirectives(config, field, gen);
if (isNonNullType(parentType)) {
if (config.notAllowEmptyString === true) {
const tsType = tsVisitor.scalars[type.name.value];
const tsType = visitor.getScalarType(type.name.value);
if (tsType === 'string') return `${gen}.min(1)`;
}
return appliedDirectivesGen;
Expand Down Expand Up @@ -192,33 +189,32 @@ const applyDirectives = (

const generateNameNodeMyZodSchema = (
config: ValidationSchemaPluginConfig,
tsVisitor: TsVisitor,
schema: GraphQLSchema,
visitor: Visitor,
node: NameNode
): string => {
const typ = schema.getType(node.value);
const converter = visitor.getNameNodeConverter(node);

if (typ?.astNode?.kind === 'InputObjectTypeDefinition') {
const enumName = tsVisitor.convertName(typ.astNode.name.value);
return `${enumName}Schema()`;
if (converter?.targetKind === 'InputObjectTypeDefinition') {
const name = converter.convertName();
return `${name}Schema()`;
}

if (typ?.astNode?.kind === 'ObjectTypeDefinition') {
const enumName = tsVisitor.convertName(typ.astNode.name.value);
return `${enumName}Schema()`;
if (converter?.targetKind === 'ObjectTypeDefinition') {
const name = converter.convertName();
return `${name}Schema()`;
}

if (typ?.astNode?.kind === 'EnumTypeDefinition') {
const enumName = tsVisitor.convertName(typ.astNode.name.value);
return `${enumName}Schema`;
if (converter?.targetKind === 'EnumTypeDefinition') {
const name = converter.convertName();
return `${name}Schema`;
}

if (typ?.astNode?.kind === 'UnionTypeDefinition') {
const enumName = tsVisitor.convertName(typ.astNode.name.value);
return `${enumName}Schema()`;
if (converter?.targetKind === 'UnionTypeDefinition') {
const name = converter.convertName();
return `${name}Schema()`;
}

return myzod4Scalar(config, tsVisitor, node.value);
return myzod4Scalar(config, visitor, node.value);
};

const maybeLazy = (type: TypeNode, schema: string): string => {
Expand All @@ -228,11 +224,11 @@ const maybeLazy = (type: TypeNode, schema: string): string => {
return schema;
};

const myzod4Scalar = (config: ValidationSchemaPluginConfig, tsVisitor: TsVisitor, scalarName: string): string => {
const myzod4Scalar = (config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string => {
if (config.scalarSchemas?.[scalarName]) {
return config.scalarSchemas[scalarName];
}
const tsType = tsVisitor.scalars[scalarName];
const tsType = visitor.getScalarType(scalarName);
switch (tsType) {
case 'string':
return `myzod.string()`;
Expand Down
36 changes: 36 additions & 0 deletions src/visitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ValidationSchemaPluginConfig } from './config';
import { TsVisitor } from '@graphql-codegen/typescript';
import { NameNode, GraphQLSchema } from 'graphql';

export class Visitor extends TsVisitor {
constructor(
private scalarDirection: 'input' | 'output' | 'both',
private schema: GraphQLSchema,
config: ValidationSchemaPluginConfig
) {
super(schema, config);
}

public getType(name: string) {
return this.schema.getType(name);
}

public getNameNodeConverter(node: NameNode) {
const typ = this.schema.getType(node.value);
const astNode = typ?.astNode;
if (astNode === undefined || astNode === null) {
return undefined;
}
return {
targetKind: astNode.kind,
convertName: () => this.convertName(astNode.name.value),
};
}

public getScalarType(scalarName: string): string | null {
if (this.scalarDirection === 'both') {
return null;
}
return this.scalars[scalarName][this.scalarDirection];
}
}
Loading