From dbc0ef81b1ba7d6130416de50d5920fda51f0ee5 Mon Sep 17 00:00:00 2001 From: Jose Cisneros Date: Sun, 13 Oct 2024 17:03:13 -0700 Subject: [PATCH 1/2] feat: add support for namespaced imports --- src/config.ts | 20 +++++ src/myzod/index.ts | 18 +++-- src/schema_visitor.ts | 10 ++- src/valibot/index.ts | 12 ++- src/visitor.ts | 12 ++- src/yup/index.ts | 27 ++++--- src/zod/index.ts | 18 +++-- tests/myzod.spec.ts | 166 ++++++++++++++++++++++++++++++++++++++++++ tests/valibot.spec.ts | 164 +++++++++++++++++++++++++++++++++++++++++ tests/yup.spec.ts | 140 ++++++++++++++++++++++++++++++++++- tests/zod.spec.ts | 166 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 716 insertions(+), 37 deletions(-) diff --git a/src/config.ts b/src/config.ts index 4e26153b..fe9fca34 100644 --- a/src/config.ts +++ b/src/config.ts @@ -54,6 +54,26 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * ``` */ importFrom?: string + /** + * @description If defined, will use named imports from the specified module (defined in `importFrom`) + * rather than individual imports for each type. + * + * @exampleMarkdown + * ```yml + * generates: + * path/to/types.ts: + * plugins: + * - typescript + * path/to/schemas.ts: + * plugins: + * - graphql-codegen-validation-schema + * config: + * schema: yup + * importFrom: ./path/to/types + * namespacedImportName: types + * ``` + */ + schemaNamespacedImportName?: string /** * @description Will use `import type {}` rather than `import {}` when importing generated typescript types. * This gives compatibility with TypeScript's "importsNotUsedAsValues": "error" option diff --git a/src/myzod/index.ts b/src/myzod/index.ts index 8804a153..f8727978 100644 --- a/src/myzod/index.ts +++ b/src/myzod/index.ts @@ -67,6 +67,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); this.importTypes.push(name); // Building schema for field arguments. @@ -82,7 +83,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('const') - .withName(`${name}Schema: myzod.Type<${name}>`) + .withName(`${name}Schema: myzod.Type<${typeName}>`) .withContent([`myzod.object({`, shape, '})'].join('\n')) .string + appendArguments ); @@ -93,7 +94,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): myzod.Type<${name}>`) + .withName(`${name}Schema(): myzod.Type<${typeName}>`) .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')) .string + appendArguments ); @@ -107,6 +108,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); this.importTypes.push(name); // Building schema for field arguments. @@ -122,7 +124,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('const') - .withName(`${name}Schema: myzod.Type<${name}>`) + .withName(`${name}Schema: myzod.Type<${typeName}>`) .withContent( [ `myzod.object({`, @@ -140,7 +142,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): myzod.Type<${name}>`) + .withName(`${name}Schema(): myzod.Type<${typeName}>`) .withBlock( [ indent(`return myzod.object({`), @@ -161,6 +163,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { leave: (node: EnumTypeDefinitionNode) => { const visitor = this.createVisitor('both'); const enumname = visitor.convertName(node.name.value); + const enumTypeName = visitor.prefixTypeNamespace(enumname); this.importTypes.push(enumname); // z.enum are basically myzod.literals // hoist enum declarations @@ -178,7 +181,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${enumname}Schema`) - .withContent(`myzod.enum(${enumname})`) + .withContent(`myzod.enum(${enumTypeName})`) .string, ); }, @@ -233,6 +236,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { visitor: Visitor, name: string, ) { + const typeName = visitor.prefixTypeNamespace(name); const shape = fields.map(field => generateFieldMyZodSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { @@ -240,7 +244,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { return new DeclarationBlock({}) .export() .asKind('const') - .withName(`${name}Schema: myzod.Type<${name}>`) + .withName(`${name}Schema: myzod.Type<${typeName}>`) .withContent(['myzod.object({', shape, '})'].join('\n')) .string; @@ -249,7 +253,7 @@ export class MyZodSchemaVisitor extends BaseSchemaVisitor { return new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): myzod.Type<${name}>`) + .withName(`${name}Schema(): myzod.Type<${typeName}>`) .withBlock([indent(`return myzod.object({`), shape, indent('})')].join('\n')) .string; } diff --git a/src/schema_visitor.ts b/src/schema_visitor.ts index 0efee81d..ca2daa66 100644 --- a/src/schema_visitor.ts +++ b/src/schema_visitor.ts @@ -23,11 +23,15 @@ export abstract class BaseSchemaVisitor implements SchemaVisitor { buildImports(): string[] { if (this.config.importFrom && this.importTypes.length > 0) { + const namedImportPrefix = this.config.useTypeImports ? 'type ' : ''; + + const importCore = this.config.schemaNamespacedImportName + ? `* as ${this.config.schemaNamespacedImportName}` + : `${namedImportPrefix}{ ${this.importTypes.join(', ')} }`; + return [ this.importValidationSchema(), - `import ${this.config.useTypeImports ? 'type ' : ''}{ ${this.importTypes.join(', ')} } from '${ - this.config.importFrom - }'`, + `import ${importCore} from '${this.config.importFrom}'`, ]; } return [this.importValidationSchema()]; diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 71c4d09b..559c2afc 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -58,6 +58,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); this.importTypes.push(name); // Building schema for field arguments. @@ -73,7 +74,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): v.GenericSchema<${name}>`) + .withName(`${name}Schema(): v.GenericSchema<${typeName}>`) .withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')) .string + appendArguments ); @@ -87,6 +88,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); this.importTypes.push(name); // Building schema for field arguments. @@ -102,7 +104,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): v.GenericSchema<${name}>`) + .withName(`${name}Schema(): v.GenericSchema<${typeName}>`) .withBlock( [ indent(`return v.object({`), @@ -123,6 +125,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { leave: (node: EnumTypeDefinitionNode) => { const visitor = this.createVisitor('both'); const enumname = visitor.convertName(node.name.value); + const enumTypeName = visitor.prefixTypeNamespace(enumname); this.importTypes.push(enumname); // hoist enum declarations @@ -138,7 +141,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${enumname}Schema`) - .withContent(`v.enum_(${enumname})`) + .withContent(`v.enum_(${enumTypeName})`) .string, ); }, @@ -185,6 +188,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { visitor: Visitor, name: string, ) { + const typeName = visitor.prefixTypeNamespace(name); const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { @@ -192,7 +196,7 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { return new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): v.GenericSchema<${name}>`) + .withName(`${name}Schema(): v.GenericSchema<${typeName}>`) .withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')) .string; } diff --git a/src/visitor.ts b/src/visitor.ts index 7866d2a1..44eab6df 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -1,4 +1,6 @@ +import type { BaseVisitorConvertOptions, ConvertOptions } from '@graphql-codegen/visitor-plugin-common'; import type { + ASTNode, FieldDefinitionNode, GraphQLSchema, InterfaceTypeDefinitionNode, @@ -6,8 +8,8 @@ import type { ObjectTypeDefinitionNode, } from 'graphql'; import type { ValidationSchemaPluginConfig } from './config.js'; -import { TsVisitor } from '@graphql-codegen/typescript'; +import { TsVisitor } from '@graphql-codegen/typescript'; import { specifiedScalarTypes, } from 'graphql'; @@ -21,6 +23,14 @@ export class Visitor extends TsVisitor { super(schema, pluginConfig); } + public prefixTypeNamespace(type: string): string { + if (this.pluginConfig.importFrom && this.pluginConfig.schemaNamespacedImportName) { + return `${this.pluginConfig.schemaNamespacedImportName}.${type}`; + } + + return type; + } + private isSpecifiedScalarName(scalarName: string) { return specifiedScalarTypes.some(({ name }) => name === scalarName); } diff --git a/src/yup/index.ts b/src/yup/index.ts index b1805d8a..557715dc 100644 --- a/src/yup/index.ts +++ b/src/yup/index.ts @@ -76,6 +76,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); this.importTypes.push(name); // Building schema for field arguments. @@ -94,7 +95,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('const') - .withName(`${name}Schema: yup.ObjectSchema<${name}>`) + .withName(`${name}Schema: yup.ObjectSchema<${typeName}>`) .withContent([`yup.object({`, shape, '})'].join('\n')) .string + appendArguments ); @@ -105,7 +106,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): yup.ObjectSchema<${name}>`) + .withName(`${name}Schema(): yup.ObjectSchema<${typeName}>`) .withBlock([indent(`return yup.object({`), shape, indent('})')].join('\n')) .string + appendArguments ); @@ -119,6 +120,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); this.importTypes.push(name); // Building schema for field arguments. @@ -134,7 +136,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('const') - .withName(`${name}Schema: yup.ObjectSchema<${name}>`) + .withName(`${name}Schema: yup.ObjectSchema<${typeName}>`) .withContent( [ `yup.object({`, @@ -152,7 +154,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): yup.ObjectSchema<${name}>`) + .withName(`${name}Schema(): yup.ObjectSchema<${typeName}>`) .withBlock( [ indent(`return yup.object({`), @@ -173,6 +175,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { leave: (node: EnumTypeDefinitionNode) => { const visitor = this.createVisitor('both'); const enumname = visitor.convertName(node.name.value); + const enumTypeName = visitor.prefixTypeNamespace(enumname); this.importTypes.push(enumname); // hoise enum declarations @@ -193,7 +196,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${enumname}Schema`) - .withContent(`yup.string<${enumname}>().oneOf(Object.values(${enumname})).defined()`).string, + .withContent(`yup.string<${enumTypeName}>().oneOf(Object.values(${enumTypeName})).defined()`).string, ); } }, @@ -208,6 +211,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { const visitor = this.createVisitor('output'); const unionName = visitor.convertName(node.name.value); + const unionTypeName = visitor.prefixTypeNamespace(unionName); this.importTypes.push(unionName); const unionElements = node.types?.map((t) => { @@ -230,16 +234,16 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { return new DeclarationBlock({}) .export() .asKind('const') - .withName(`${unionName}Schema: yup.MixedSchema<${unionName}>`) - .withContent(`union<${unionName}>(${unionElements})`) + .withName(`${unionName}Schema: yup.MixedSchema<${unionTypeName}>`) + .withContent(`union<${unionTypeName}>(${unionElements})`) .string; case 'function': default: return new DeclarationBlock({}) .export() .asKind('function') - .withName(`${unionName}Schema(): yup.MixedSchema<${unionName}>`) - .withBlock(indent(`return union<${unionName}>(${unionElements})`)) + .withName(`${unionName}Schema(): yup.MixedSchema<${unionTypeName}>`) + .withBlock(indent(`return union<${unionTypeName}>(${unionElements})`)) .string; } }, @@ -251,6 +255,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { visitor: Visitor, name: string, ) { + const typeName = visitor.prefixTypeNamespace(name); const shape = shapeFields(fields, this.config, visitor); switch (this.config.validationSchemaExportType) { @@ -258,7 +263,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { return new DeclarationBlock({}) .export() .asKind('const') - .withName(`${name}Schema: yup.ObjectSchema<${name}>`) + .withName(`${name}Schema: yup.ObjectSchema<${typeName}>`) .withContent(['yup.object({', shape, '})'].join('\n')) .string; @@ -267,7 +272,7 @@ export class YupSchemaVisitor extends BaseSchemaVisitor { return new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): yup.ObjectSchema<${name}>`) + .withName(`${name}Schema(): yup.ObjectSchema<${typeName}>`) .withBlock([indent(`return yup.object({`), shape, indent('})')].join('\n')) .string; } diff --git a/src/zod/index.ts b/src/zod/index.ts index b1164fb0..358ec8d5 100644 --- a/src/zod/index.ts +++ b/src/zod/index.ts @@ -87,6 +87,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); this.importTypes.push(name); // Building schema for field arguments. @@ -102,7 +103,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('const') - .withName(`${name}Schema: z.ZodObject>`) + .withName(`${name}Schema: z.ZodObject>`) .withContent([`z.object({`, shape, '})'].join('\n')) .string + appendArguments ); @@ -113,7 +114,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): z.ZodObject>`) + .withName(`${name}Schema(): z.ZodObject>`) .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')) .string + appendArguments ); @@ -127,6 +128,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { const visitor = this.createVisitor('output'); const name = visitor.convertName(node.name.value); + const typeName = visitor.prefixTypeNamespace(name); this.importTypes.push(name); // Building schema for field arguments. @@ -142,7 +144,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('const') - .withName(`${name}Schema: z.ZodObject>`) + .withName(`${name}Schema: z.ZodObject>`) .withContent( [ `z.object({`, @@ -160,7 +162,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): z.ZodObject>`) + .withName(`${name}Schema(): z.ZodObject>`) .withBlock( [ indent(`return z.object({`), @@ -181,6 +183,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { leave: (node: EnumTypeDefinitionNode) => { const visitor = this.createVisitor('both'); const enumname = visitor.convertName(node.name.value); + const enumTypeName = visitor.prefixTypeNamespace(enumname); this.importTypes.push(enumname); // hoist enum declarations @@ -196,7 +199,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { .export() .asKind('const') .withName(`${enumname}Schema`) - .withContent(`z.nativeEnum(${enumname})`) + .withContent(`z.nativeEnum(${enumTypeName})`) .string, ); }, @@ -249,6 +252,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { visitor: Visitor, name: string, ) { + const typeName = visitor.prefixTypeNamespace(name); const shape = fields.map(field => generateFieldZodSchema(this.config, visitor, field, 2)).join(',\n'); switch (this.config.validationSchemaExportType) { @@ -256,7 +260,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { return new DeclarationBlock({}) .export() .asKind('const') - .withName(`${name}Schema: z.ZodObject>`) + .withName(`${name}Schema: z.ZodObject>`) .withContent(['z.object({', shape, '})'].join('\n')) .string; @@ -265,7 +269,7 @@ export class ZodSchemaVisitor extends BaseSchemaVisitor { return new DeclarationBlock({}) .export() .asKind('function') - .withName(`${name}Schema(): z.ZodObject>`) + .withName(`${name}Schema(): z.ZodObject>`) .withBlock([indent(`return z.object({`), shape, indent('})')].join('\n')) .string; } diff --git a/tests/myzod.spec.ts b/tests/myzod.spec.ts index 0497476e..7b6f062a 100644 --- a/tests/myzod.spec.ts +++ b/tests/myzod.spec.ts @@ -156,6 +156,43 @@ describe('myzod', () => { `) }); + it('ref input object w/ schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'myzod', scalars, importFrom: './types', schemaNamespacedImportName: 't' }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export function AInputSchema(): myzod.Type { + return myzod.object({ + b: myzod.lazy(() => BInputSchema()) + }) + } + + export function BInputSchema(): myzod.Type { + return myzod.object({ + c: myzod.lazy(() => CInputSchema()) + }) + } + + export function CInputSchema(): myzod.Type { + return myzod.object({ + a: myzod.lazy(() => AInputSchema()) + }) + } + " + `) + }) + it('nested input object', async () => { const schema = buildSchema(` input NestedInput { @@ -204,6 +241,31 @@ describe('myzod', () => { `) }); + it('enum w/ schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'myzod', scalars, importFrom: './', schemaNamespacedImportName: 't' }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export const PageTypeSchema = myzod.enum(t.PageType); + + export function PageInputSchema(): myzod.Type { + return myzod.object({ + pageType: PageTypeSchema + }) + } + " + `) + }) + it('camelcase', async () => { const schema = buildSchema(` input HTTPInput { @@ -301,6 +363,39 @@ describe('myzod', () => { `) }); + it('with importFrom & schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as myzod from 'myzod'", + "import * as t from './types'", + ] + `) + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export function SaySchema(): myzod.Type { + return myzod.object({ + phrase: myzod.string() + }) + } + " + `) + }); + it('with importFrom & useTypeImports', async () => { const schema = buildSchema(/* GraphQL */ ` input Say { @@ -357,6 +452,31 @@ describe('myzod', () => { `) }); + it('with enumsAsTypes + schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + schema: 'myzod', + enumsAsTypes: true, + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export type PageTypeSchema = myzod.literals('PUBLIC', 'BASIC_AUTH'); + " + `) + }); + it('with notAllowEmptyString', async () => { const schema = buildSchema(/* GraphQL */ ` input PrimitiveInput { @@ -826,6 +946,52 @@ describe('myzod', () => { `) }); + it('generate union types + schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + `); + + const result = await plugin( + schema, + [], + { + schema: 'myzod', + withObjectType: true, + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export function SquareSchema(): myzod.Type { + return myzod.object({ + __typename: myzod.literal('Square').optional(), + size: myzod.number().optional().nullable() + }) + } + + export function CircleSchema(): myzod.Type { + return myzod.object({ + __typename: myzod.literal('Circle').optional(), + radius: myzod.number().optional().nullable() + }) + } + + export function ShapeSchema() { + return myzod.union([CircleSchema(), SquareSchema()]) + } + " + `) + }); + it('generate union types with single element', async () => { const schema = buildSchema(/* GraphQL */ ` type Square { diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 61f5a535..813131cc 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -129,6 +129,43 @@ describe('valibot', () => { " `); }) + it('ref input object w/ schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars, importFrom: './types', schemaNamespacedImportName: 't' }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + + export function AInputSchema(): v.GenericSchema { + return v.object({ + b: v.lazy(() => BInputSchema()) + }) + } + + export function BInputSchema(): v.GenericSchema { + return v.object({ + c: v.lazy(() => CInputSchema()) + }) + } + + export function CInputSchema(): v.GenericSchema { + return v.object({ + a: v.lazy(() => AInputSchema()) + }) + } + " + `); + }) it.todo('nested input object') it('enum', async () => { const schema = buildSchema(/* GraphQL */ ` @@ -154,6 +191,30 @@ describe('valibot', () => { " `); }) + it('enum w/ schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars, importFrom: './types', schemaNamespacedImportName: 't' }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = v.enum_(t.PageType); + + export function PageInputSchema(): v.GenericSchema { + return v.object({ + pageType: PageTypeSchema + }) + } + " + `); + }) it('camelcase', async () => { const schema = buildSchema(/* GraphQL */ ` input HTTPInput { @@ -246,6 +307,39 @@ describe('valibot', () => { " `); }); + it('with importFrom & schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as v from 'valibot'", + "import * as t from './types'", + ] + `); + expect(result.content).toMatchInlineSnapshot(` + " + + export function SaySchema(): v.GenericSchema { + return v.object({ + phrase: v.string() + }) + } + " + `); + }); it('with importFrom & useTypeImports', async () => { const schema = buildSchema(/* GraphQL */ ` input Say { @@ -301,6 +395,30 @@ describe('valibot', () => { " `); }); + it('with enumsAsTypes & schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + enumsAsTypes: true, + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = v.picklist([\'PUBLIC\', \'BASIC_AUTH\']); + " + `); + }); it.todo('with notAllowEmptyString') it.todo('with notAllowEmptyString issue #386') it('with scalarSchemas', async () => { @@ -610,6 +728,52 @@ describe('valibot', () => { `) }); }) + it('generate union types & schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + `); + + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function SquareSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Square')), + size: v.nullish(v.number()) + }) + } + + export function CircleSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Circle')), + radius: v.nullish(v.number()) + }) + } + + export function ShapeSchema() { + return v.union([CircleSchema(), SquareSchema()]) + } + " + `) + }); it('generate union types with single element', async () => { const schema = buildSchema(/* GraphQL */ ` type Square { diff --git a/tests/yup.spec.ts b/tests/yup.spec.ts index bad9c1f4..69d24735 100644 --- a/tests/yup.spec.ts +++ b/tests/yup.spec.ts @@ -144,6 +144,46 @@ describe('yup', () => { `) }); + it('ref input object w/ schemaNamespacedImportName', async () => { + const textSchema = /* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `; + + const schema = buildSchema(textSchema); + const result = await plugin(schema, [], { importFrom: './types', schemaNamespacedImportName: 't' }, {}); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function AInputSchema(): yup.ObjectSchema { + return yup.object({ + b: yup.lazy(() => BInputSchema().nonNullable()) + }) + } + + export function BInputSchema(): yup.ObjectSchema { + return yup.object({ + c: yup.lazy(() => CInputSchema().nonNullable()) + }) + } + + export function CInputSchema(): yup.ObjectSchema { + return yup.object({ + a: yup.lazy(() => AInputSchema().nonNullable()) + }) + } + " + `) + }); + it('nested input object', async () => { const schema = buildSchema(/* GraphQL */ ` input NestedInput { @@ -203,6 +243,39 @@ describe('yup', () => { `) }); + it('enum w/ schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `); + const result = await plugin( + schema, + [], + { + scalars: undefined, + importFrom: './', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = yup.string().oneOf(Object.values(t.PageType)).defined(); + + export function PageInputSchema(): yup.ObjectSchema { + return yup.object({ + pageType: PageTypeSchema.nonNullable() + }) + } + " + `) + }); + it('camelcase', async () => { const schema = buildSchema(/* GraphQL */ ` input HTTPInput { @@ -305,6 +378,39 @@ describe('yup', () => { `) }); + it('with importFrom & schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as yup from 'yup'", + "import * as t from './types'", + ] + `) + expect(result.content).toMatchInlineSnapshot(` + " + + export function SaySchema(): yup.ObjectSchema { + return yup.object({ + phrase: yup.string().defined().nonNullable() + }) + } + " + `) + }); + it('with importFrom & useTypeImports', async () => { const schema = buildSchema(/* GraphQL */ ` input Say { @@ -360,6 +466,30 @@ describe('yup', () => { `) }); + it('with enumsAsTypes + schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + enumsAsTypes: true, + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = yup.string().oneOf(['PUBLIC', 'BASIC_AUTH']).defined(); + " + `) + }); + it('with notAllowEmptyString', async () => { const schema = buildSchema(/* GraphQL */ ` input PrimitiveInput { @@ -735,6 +865,8 @@ describe('yup', () => { { schema: 'yup', withObjectType: true, + importFrom: './types', + schemaNamespacedImportName: 't', }, {}, ); @@ -748,22 +880,22 @@ describe('yup', () => { }).defined() } - export function SquareSchema(): yup.ObjectSchema { + export function SquareSchema(): yup.ObjectSchema { return yup.object({ __typename: yup.string<'Square'>().optional(), size: yup.number().defined().nullable().optional() }) } - export function CircleSchema(): yup.ObjectSchema { + export function CircleSchema(): yup.ObjectSchema { return yup.object({ __typename: yup.string<'Circle'>().optional(), radius: yup.number().defined().nullable().optional() }) } - export function ShapeSchema(): yup.MixedSchema { - return union(CircleSchema(), SquareSchema()) + export function ShapeSchema(): yup.MixedSchema { + return union(CircleSchema(), SquareSchema()) } " `) diff --git a/tests/zod.spec.ts b/tests/zod.spec.ts index 28cbe369..ec3cc6b3 100644 --- a/tests/zod.spec.ts +++ b/tests/zod.spec.ts @@ -155,6 +155,43 @@ describe('zod', () => { `) }) + it('ref input object w/ schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'zod', scalars, importFrom: './types', schemaNamespacedImportName: 't' }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export function AInputSchema(): z.ZodObject> { + return z.object({ + b: z.lazy(() => BInputSchema()) + }) + } + + export function BInputSchema(): z.ZodObject> { + return z.object({ + c: z.lazy(() => CInputSchema()) + }) + } + + export function CInputSchema(): z.ZodObject> { + return z.object({ + a: z.lazy(() => AInputSchema()) + }) + } + " + `) + }) + it('nested input object', async () => { const schema = buildSchema(/* GraphQL */ ` input NestedInput { @@ -201,6 +238,31 @@ describe('zod', () => { `) }) + it('enum w/ schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'zod', scalars, importFrom: './', schemaNamespacedImportName: 't' }, {}); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export const PageTypeSchema = z.nativeEnum(t.PageType); + + export function PageInputSchema(): z.ZodObject> { + return z.object({ + pageType: PageTypeSchema + }) + } + " + `) + }) + it('camelcase', async () => { const schema = buildSchema(/* GraphQL */ ` input HTTPInput { @@ -330,6 +392,39 @@ describe('zod', () => { `) }); + it('with importFrom & schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import { z } from 'zod'", + "import * as t from './types'", + ] + `); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export function SaySchema(): z.ZodObject> { + return z.object({ + phrase: z.string() + }) + } + " + `) + }); + it('with enumsAsTypes', async () => { const schema = buildSchema(/* GraphQL */ ` enum PageType { @@ -353,6 +448,31 @@ describe('zod', () => { `) }); + it('with enumsAsTypes + schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + schema: 'zod', + enumsAsTypes: true, + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export const PageTypeSchema = z.enum(['PUBLIC', 'BASIC_AUTH']); + " + `) + }); + it('with notAllowEmptyString', async () => { const schema = buildSchema(/* GraphQL */ ` input PrimitiveInput { @@ -977,6 +1097,52 @@ describe('zod', () => { `) }); + it('generate union types + schemaNamespacedImportName', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + `); + + const result = await plugin( + schema, + [], + { + schema: 'zod', + withObjectType: true, + importFrom: './types', + schemaNamespacedImportName: 't', + }, + {}, + ); + + expect(removedInitialEmitValue(result.content)).toMatchInlineSnapshot(` + " + export function SquareSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Square').optional(), + size: z.number().nullish() + }) + } + + export function CircleSchema(): z.ZodObject> { + return z.object({ + __typename: z.literal('Circle').optional(), + radius: z.number().nullish() + }) + } + + export function ShapeSchema() { + return z.union([CircleSchema(), SquareSchema()]) + } + " + `) + }); + it('generate union types with single element', async () => { const schema = buildSchema(/* GraphQL */ ` type Square { From 8390d20ff558be766dbb6e464ed168b3c0c2d897 Mon Sep 17 00:00:00 2001 From: Jose Cisneros Date: Thu, 7 Nov 2024 08:05:57 -0800 Subject: [PATCH 2/2] chore(ns-imports): fix README + config desc --- README.md | 28 ++++++++++++++++++++++++++++ src/config.ts | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fd6cdae6..7b1fe171 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,34 @@ import { GeneratedInput } from './graphql' /* generates validation schema here */ ``` +### `schemaNamespacedImportName` + +type: `string` + +If defined, will use named imports from the specified module (defined in `importFrom`) rather than individual imports for each type. + +```yml +generates: + path/to/types.ts: + plugins: + - typescript + path/to/schemas.ts: + plugins: + - graphql-codegen-validation-schema + config: + schema: yup + importFrom: ./path/to/types + schemaNamespacedImportName: types +``` + +Then the generator generates code with import statement like below. + +```ts +import * as types from './graphql' + +/* generates validation schema here */ +``` + ### `useTypeImports` type: `boolean` default: `false` diff --git a/src/config.ts b/src/config.ts index d59c8ad3..b80659d6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -70,7 +70,7 @@ export interface ValidationSchemaPluginConfig extends TypeScriptPluginConfig { * config: * schema: yup * importFrom: ./path/to/types - * namespacedImportName: types + * schemaNamespacedImportName: types * ``` */ schemaNamespacedImportName?: string