├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── codegen.yml ├── package.json ├── schema.graphql ├── src ├── config.ts ├── index.ts └── visitor.ts ├── test ├── .eslintrc.js ├── __snapshots__ │ └── pydantic.test.ts.snap └── pydantic.test.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@satel/ts-react'], 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | test.py 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | .env* 27 | 28 | 29 | *.log 30 | .DS_Store 31 | node_modules 32 | dist 33 | 34 | .mypy_cache/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Quinn Blenkinsop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > Unfortunatly, I never did find the time to flesh out this plugin & I've moved on from both GraphQL & Python for the time being. 3 | > 4 | > If you are looking for alternatives, consider [@jhnnsrs's turms](https://github.com/jhnnsrs/turms) (see [#10](https://github.com/qw-in/graphql-codegen-pydantic/issues/10)). 5 | > 6 | > Please feel free to contact me if you are interested in taking over this package. Thanks! 7 | > 8 | > --Quinn (@qw-in) 9 | 10 | 11 | # Pydantic type generation for graphql 12 | 13 | `graphql-codegen-pydantic` is a plugin for [`graphql-codegen`](https://graphql-code-generator.com/docs/getting-started/) 14 | that generates [pydantic](https://pydantic-docs.helpmanual.io/) types from any graphql schema 15 | 16 | ## Example 17 | 18 | ```graphql 19 | type Book { 20 | title: String 21 | author: Author 22 | } 23 | 24 | type Author { 25 | name: String 26 | books: [Book] 27 | } 28 | ``` 29 | 30 | becomes 31 | 32 | ```python 33 | from typing import Optional, List 34 | from pydantic import BaseModel 35 | 36 | 37 | class Author(BaseModel): 38 | name: Optional[str] 39 | books: Optional[List[Optional['Book']]] 40 | 41 | 42 | class Book(BaseModel): 43 | title: Optional[str] 44 | author: Optional['Author'] 45 | ``` 46 | 47 | ## Warning 48 | 49 | `graphql-codegen-pydantic` is currently still very experimental and is **not ready for production use** 50 | 51 | ## Installation 52 | 53 | 1. Set up [`graphql-codegen`](https://graphql-code-generator.com/docs/getting-started/) 54 | 2. Install `graphql-codegen-pydantic` 55 | ```shell 56 | yarn add graphql-codegen-pydantic -D 57 | ``` 58 | 3. Add python file to `codegen.yml` 59 | ```yml 60 | schema: http://localhost:3000/graphql 61 | generates: 62 | ./src/schema.py: 63 | plugins: 64 | - pydantic 65 | ``` 66 | 67 | ## Limitations 68 | 69 | Currently very limited 70 | 1. No configuration supported 71 | 1. No comments included in generated code 72 | 1. No support for documents 73 | 1. No resolver support for eg graphene or ariadne 74 | 1. Properties converted to `snake_case` 75 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | schema: schema.graphql 2 | generates: 3 | ./test.py: 4 | plugins: 5 | - dist -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.1", 3 | "license": "MIT", 4 | "name": "graphql-codegen-pydantic", 5 | "author": "Quinn Blenkinsop", 6 | "main": "dist/index.js", 7 | "module": "dist/graphql-codegen-pydantic.esm.js", 8 | "typings": "dist/index.d.ts", 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "engines": { 14 | "node": ">=10" 15 | }, 16 | "scripts": { 17 | "start": "tsdx watch --target node", 18 | "build": "tsdx build --target node", 19 | "test": "tsdx test", 20 | "gen": "graphql-codegen", 21 | "prepare": "yarn build" 22 | }, 23 | "dependencies": { 24 | "@graphql-codegen/plugin-helpers": "^1.13.1", 25 | "@graphql-codegen/visitor-plugin-common": "^1.13.1", 26 | "change-case": "4.1.1", 27 | "dependency-graph": "0.9.0" 28 | }, 29 | "peerDependencies": { 30 | "@graphql-codegen/cli": "^1.13.1", 31 | "graphql": "^14.6.0" 32 | }, 33 | "devDependencies": { 34 | "@graphql-codegen/cli": "1.13.1", 35 | "@graphql-codegen/testing": "1.13.1", 36 | "@satel/eslint-config-ts-react": "3.3.1", 37 | "@types/jest": "25.1.4", 38 | "eslint": "6.8.0", 39 | "graphql": "14.6.0", 40 | "tsdx": "0.13.0", 41 | "tslib": "1.11.1", 42 | "typescript": "3.8.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | type Book { 2 | title: String 3 | author: Author 4 | } 5 | 6 | type Author { 7 | name: String 8 | books: [Book] 9 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { RawConfig } from '@graphql-codegen/visitor-plugin-common'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 4 | export interface PydanticPluginRawConfig extends RawConfig { 5 | /* intentionally empty for now */ 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { parse, GraphQLSchema, printSchema, visit } from 'graphql'; 2 | import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; 3 | 4 | import { PydanticVisitor } from './visitor'; 5 | import { PydanticPluginRawConfig } from './config'; 6 | 7 | // eslint-disable-next-line import/prefer-default-export 8 | export const plugin: PluginFunction = async ( 9 | schema: GraphQLSchema, 10 | documents: Types.DocumentFile[], 11 | config: PydanticPluginRawConfig, 12 | info, 13 | ): Promise => { 14 | const visitor = new PydanticVisitor(config, schema); 15 | const printedSchema = printSchema(schema); 16 | const astNode = parse(printedSchema); 17 | 18 | const visitorResult = visit(astNode, { leave: visitor as any }); 19 | const imports = visitor.getImports(); 20 | 21 | return `${imports}\n\n\n${visitorResult}\n`; 22 | }; 23 | -------------------------------------------------------------------------------- /src/visitor.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable lines-between-class-members */ 2 | /* eslint-disable class-methods-use-this */ 3 | /* eslint-disable react/no-this-in-sfc */ 4 | import { 5 | BaseVisitor, 6 | ParsedConfig, 7 | buildScalars, 8 | indent, 9 | } from '@graphql-codegen/visitor-plugin-common'; 10 | import { 11 | NamedTypeNode, 12 | ListTypeNode, 13 | NonNullTypeNode, 14 | GraphQLSchema, 15 | FieldDefinitionNode, 16 | ObjectTypeDefinitionNode, 17 | NameNode, 18 | UnionTypeDefinitionNode, 19 | DocumentNode, 20 | InterfaceTypeDefinitionNode, 21 | EnumTypeDefinitionNode, 22 | InputObjectTypeDefinitionNode, 23 | InputValueDefinitionNode, 24 | } from 'graphql'; 25 | import { snakeCase } from 'change-case'; 26 | import { DepGraph } from 'dependency-graph'; 27 | 28 | import { PydanticPluginRawConfig } from './config'; 29 | 30 | export const PYTHON_SCALARS = { 31 | ID: 'str', 32 | String: 'str', 33 | Boolean: 'bool', 34 | Int: 'int', 35 | Float: 'float', 36 | }; 37 | 38 | const PYTHON_RESERVED = ['from']; 39 | const PYDANTIC_MODEL_RESERVED = ['copy']; 40 | const RESERVED = PYTHON_RESERVED.concat(PYDANTIC_MODEL_RESERVED); 41 | 42 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 43 | export interface PydanticPluginParsedConfig extends ParsedConfig { 44 | /* intentionally empty for now */ 45 | } 46 | 47 | export class PydanticVisitor extends BaseVisitor< 48 | PydanticPluginRawConfig, 49 | PydanticPluginParsedConfig 50 | > { 51 | private addOptionalImport = false; 52 | private addAnyImport = false; 53 | private addListImport = false; 54 | private addUnionImport = false; 55 | private addEnumImport = false; 56 | private addFieldImport = false; 57 | 58 | private graph = new DepGraph({ 59 | circular: false, 60 | }); 61 | 62 | constructor( 63 | rawConfig: PydanticPluginRawConfig, 64 | private schema: GraphQLSchema, 65 | ) { 66 | super(rawConfig, { 67 | // enumValues: rawConfig.enumValues || {}, 68 | // listType: rawConfig.listType || 'List', 69 | // package: rawConfig.package || defaultPackageName, 70 | scalars: buildScalars(schema, {}, PYTHON_SCALARS), 71 | }); 72 | } 73 | 74 | public getImports(): string { 75 | const typing = []; 76 | const pydantic = ['BaseModel']; 77 | 78 | if (this.addAnyImport) { 79 | typing.push(`Any`); 80 | } 81 | 82 | if (this.addOptionalImport) { 83 | typing.push(`Optional`); 84 | } 85 | 86 | if (this.addListImport) { 87 | typing.push(`List`); 88 | } 89 | 90 | if (this.addUnionImport) { 91 | typing.push(`Union`); 92 | } 93 | 94 | if (this.addFieldImport) { 95 | pydantic.push(`Field`); 96 | } 97 | 98 | const enumInput = this.addEnumImport ? 'from enum import Enum' : ''; 99 | 100 | const typingImport = typing.length 101 | ? `from typing import ${typing.join(', ')}` 102 | : ''; 103 | 104 | const pydanticImport = pydantic.length 105 | ? `from pydantic import ${pydantic.join(', ')}` 106 | : ''; 107 | 108 | return [enumInput, typingImport, pydanticImport].filter(i => i).join('\n'); 109 | } 110 | 111 | protected canAddGraphNode(id: string): boolean { 112 | if (Object.values(this.scalars).includes(id) || id === 'Any') { 113 | return false; 114 | } 115 | 116 | return true; 117 | } 118 | 119 | protected upsertGraphNode(id: string) { 120 | if (this.canAddGraphNode(id) && !this.graph.hasNode(id)) { 121 | this.graph.addNode(id); 122 | } 123 | } 124 | 125 | protected addGraphNodeDeps(id: string, ids: string[]) { 126 | if (!this.canAddGraphNode(id)) { 127 | return; 128 | } 129 | 130 | this.upsertGraphNode(id); 131 | 132 | ids.forEach((i: string) => { 133 | if (!this.canAddGraphNode(i)) { 134 | return; 135 | } 136 | 137 | this.upsertGraphNode(i); 138 | 139 | this.graph.addDependency(id, i); 140 | }); 141 | } 142 | 143 | protected clearOptional(str: string): string { 144 | if (str.startsWith('Optional[')) { 145 | return str.replace(/Optional\[(.*?)\]$/, '$1'); 146 | } 147 | 148 | return str; 149 | } 150 | 151 | Name(node: NameNode) { 152 | return node.value; 153 | } 154 | 155 | NamedType(node: NamedTypeNode) { 156 | const { name } = node as any; 157 | 158 | // Scalars 159 | if (Object.keys(this.scalars).includes(name)) { 160 | const id = this.scalars[name]; 161 | 162 | // Special case for any 163 | if (id === 'any') { 164 | this.addAnyImport = true; 165 | return { 166 | id: 'Any', 167 | source: 'Any', 168 | }; 169 | } 170 | 171 | this.addOptionalImport = true; 172 | return { 173 | id, 174 | source: `Optional[${id}]`, 175 | }; 176 | } 177 | 178 | // Defined 179 | this.addOptionalImport = true; 180 | return { 181 | id: name, 182 | source: `Optional['${name}']`, 183 | }; 184 | } 185 | 186 | ListType(node: ListTypeNode) { 187 | this.addListImport = true; 188 | this.addOptionalImport = true; 189 | 190 | const { type } = node as any; 191 | 192 | return { 193 | id: type.id, 194 | source: `Optional[List[${type.source}]]`, 195 | }; 196 | } 197 | 198 | NonNullType(node: NonNullTypeNode) { 199 | const { type } = node as any; 200 | 201 | return { 202 | id: type.id, 203 | source: this.clearOptional(type.source), 204 | }; 205 | } 206 | 207 | protected visitFieldOrInputDefinition(node: any) { 208 | const argName = snakeCase(node.name as any); 209 | 210 | const { type, directives } = node as any; 211 | 212 | // @todo handle de-duplicating if snakeCase will break 213 | // eg aaaa and AAAA field 214 | 215 | // Handle deprecated 216 | const ds = directives.map((d: any) => d.name); 217 | if (ds.includes('deprecated')) { 218 | return null; 219 | } 220 | 221 | // Need to alias some field names 222 | // Otherwise pydantic throws 223 | if (RESERVED.includes(argName)) { 224 | this.addFieldImport = true; 225 | return { 226 | id: type.id, 227 | source: indent( 228 | `${argName}_: ${type.source} = Field(None, alias='${argName}')`, 229 | 2, 230 | ), 231 | }; 232 | } 233 | 234 | return { 235 | id: type.id, 236 | source: indent(`${argName}: ${type.source}`, 2), 237 | }; 238 | } 239 | 240 | FieldDefinition(node: FieldDefinitionNode) { 241 | return this.visitFieldOrInputDefinition(node); 242 | } 243 | 244 | InputValueDefinition(node: InputValueDefinitionNode) { 245 | return this.visitFieldOrInputDefinition(node); 246 | } 247 | 248 | EnumTypeDefinition(node: EnumTypeDefinitionNode) { 249 | this.addEnumImport = true; 250 | 251 | const { name, values } = node as any; 252 | 253 | const val = values 254 | .map((v: any) => indent(`${v.name} = '${v.name}'`, 2)) 255 | .join('\n'); 256 | const source = `class ${name}(str, Enum):\n${val}`; 257 | 258 | this.upsertGraphNode(name); 259 | 260 | return { 261 | id: name, 262 | source, 263 | }; 264 | } 265 | 266 | UnionTypeDefinition(node: UnionTypeDefinitionNode) { 267 | this.addUnionImport = true; 268 | 269 | const { name, types } = node as any; 270 | 271 | const unionTypes = (types ?? []).map((t: any) => 272 | this.clearOptional(t.source), 273 | ); 274 | 275 | this.addGraphNodeDeps( 276 | name, 277 | types.map((t: any) => t.id), 278 | ); 279 | 280 | return { 281 | id: name, 282 | source: `${name} = Union[${unionTypes.join(', ')}]`, 283 | }; 284 | } 285 | 286 | InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode) { 287 | const { name, fields: rawFields } = node as any; 288 | 289 | const fields = rawFields.filter((f: any) => f); 290 | 291 | const args = fields.map((f: any) => f.source).join('\n'); 292 | const source = `class ${name}(BaseModel):\n${args}`; 293 | 294 | this.addGraphNodeDeps( 295 | name, 296 | fields.map((f: any) => f.id), 297 | ); 298 | 299 | return { 300 | id: name, 301 | source, 302 | }; 303 | } 304 | 305 | ObjectTypeDefinition(node: ObjectTypeDefinitionNode) { 306 | const { name, fields: rawFields, interfaces: rawInterfaces } = node as any; 307 | 308 | const fields = rawFields.filter((f: any) => f); 309 | 310 | const interfaces = rawInterfaces.map((n: any) => 311 | this.clearOptional(n.source).replace(/'/g, ''), 312 | ); 313 | 314 | const impl = interfaces.length ? interfaces.join(', ') : 'BaseModel'; 315 | 316 | const args = fields.map((f: any) => f.source).join('\n'); 317 | const source = `class ${name}(${impl}):\n${args}`; 318 | 319 | if (interfaces.length) { 320 | this.addGraphNodeDeps(name, interfaces); 321 | } else { 322 | this.upsertGraphNode(name); 323 | } 324 | 325 | return { 326 | id: name, 327 | source, 328 | }; 329 | } 330 | 331 | InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode) { 332 | const { name, fields: rawFields } = node as any; 333 | 334 | const fields = rawFields.filter((f: any) => f); 335 | 336 | const args = fields.map((f: any) => f.source).join('\n'); 337 | const source = `class ${name}(BaseModel):\n${args}`; 338 | 339 | this.upsertGraphNode(name); 340 | 341 | return { 342 | id: name, 343 | source, 344 | }; 345 | } 346 | 347 | Document(node: DocumentNode) { 348 | const { definitions } = node as any; 349 | 350 | const nodesInOrder = this.graph.overallOrder(); 351 | 352 | return nodesInOrder 353 | .map((n: any) => definitions.find((d: any) => d.id === n)?.source || '') 354 | .join('\n\n\n'); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } -------------------------------------------------------------------------------- /test/__snapshots__/pydantic.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Pydantic interface with union 1`] = ` 4 | "from typing import Optional, Union 5 | from pydantic import BaseModel 6 | 7 | 8 | class Node(BaseModel): 9 | id: str 10 | 11 | 12 | class BasePost(BaseModel): 13 | id: str 14 | title: str 15 | 16 | 17 | class TextPost(Node, BasePost): 18 | id: str 19 | title: str 20 | description: Optional[str] 21 | 22 | 23 | class ImagePost(Node, BasePost): 24 | id: str 25 | title: str 26 | source: str 27 | 28 | 29 | Post = Union['TextPost', 'ImagePost'] 30 | " 31 | `; 32 | -------------------------------------------------------------------------------- /test/pydantic.test.ts: -------------------------------------------------------------------------------- 1 | import '@graphql-codegen/testing'; 2 | import { buildSchema } from 'graphql'; 3 | 4 | import { plugin } from '../src/index'; 5 | 6 | describe('Pydantic', () => { 7 | it('basic Object', async () => { 8 | const schema = buildSchema(/* GraphQL */ ` 9 | type A { 10 | id: ID 11 | } 12 | `); 13 | 14 | const result = await plugin(schema, [], {}); 15 | 16 | expect(result).toBeSimilarStringTo(` 17 | from typing import Optional 18 | from pydantic import BaseModel 19 | 20 | 21 | class A(BaseModel): 22 | id: Optional[str] 23 | `); 24 | }); 25 | 26 | it('basic Input', async () => { 27 | const schema = buildSchema(/* GraphQL */ ` 28 | input PostUpdateInput { 29 | id: ID! 30 | title: String 31 | description: String 32 | } 33 | `); 34 | 35 | const result = await plugin(schema, [], {}); 36 | 37 | expect(result).toBeSimilarStringTo(` 38 | from typing import Optional 39 | from pydantic import BaseModel 40 | 41 | 42 | class PostUpdateInput(BaseModel): 43 | id: str 44 | title: Optional[str] 45 | description: Optional[str] 46 | `); 47 | }); 48 | 49 | it('basic Enum', async () => { 50 | const schema = buildSchema(/* GraphQL */ ` 51 | enum Type { 52 | FIRST 53 | SECOND 54 | THIRD 55 | } 56 | `); 57 | 58 | const result = await plugin(schema, [], {}); 59 | 60 | expect(result).toBeSimilarStringTo(` 61 | from enum import Enum 62 | from pydantic import BaseModel 63 | 64 | 65 | class Type(str, Enum): 66 | FIRST = 'FIRST' 67 | SECOND = 'SECOND' 68 | THIRD = 'THIRD' 69 | `); 70 | }); 71 | 72 | it('basic Union', async () => { 73 | const schema = buildSchema(/* GraphQL */ ` 74 | union U = String | Int 75 | `); 76 | 77 | const result = await plugin(schema, [], {}); 78 | 79 | // @todo fix the Optional import 80 | expect(result).toBeSimilarStringTo(` 81 | from typing import Optional, Union 82 | from pydantic import BaseModel 83 | 84 | 85 | U = Union[str, int] 86 | `); 87 | }); 88 | 89 | it('basic Interface', async () => { 90 | const schema = buildSchema(/* GraphQL */ ` 91 | interface Node { 92 | id: ID! 93 | } 94 | 95 | type Post implements Node { 96 | id: ID! 97 | title: String! 98 | description: String 99 | } 100 | `); 101 | 102 | const result = await plugin(schema, [], {}); 103 | 104 | expect(result).toBeSimilarStringTo(` 105 | from typing import Optional 106 | from pydantic import BaseModel 107 | 108 | 109 | class Node(BaseModel): 110 | id: str 111 | 112 | 113 | class Post(Node): 114 | id: str 115 | title: str 116 | description: Optional[str] 117 | `); 118 | }); 119 | 120 | it('interface with union', async () => { 121 | const schema = buildSchema(/* GraphQL */ ` 122 | interface Node { 123 | id: ID! 124 | } 125 | 126 | interface BasePost { 127 | id: ID! 128 | title: String! 129 | } 130 | 131 | type TextPost implements Node & BasePost { 132 | id: ID! 133 | title: String! 134 | description: String 135 | } 136 | 137 | type ImagePost implements Node & BasePost { 138 | id: ID! 139 | title: String! 140 | source: String! 141 | } 142 | 143 | union Post = TextPost | ImagePost 144 | `); 145 | 146 | const result = await plugin(schema, [], {}); 147 | 148 | expect(result).toMatchSnapshot(); 149 | }); 150 | 151 | it('custom scalar defaults to Any', async () => { 152 | const schema = buildSchema(/* GraphQL */ ` 153 | scalar JSON 154 | 155 | type Blob { 156 | data: JSON 157 | } 158 | `); 159 | 160 | const result = await plugin(schema, [], {}); 161 | 162 | expect(result).toBeSimilarStringTo(` 163 | from typing import Any 164 | from pydantic import BaseModel 165 | 166 | 167 | class Blob(BaseModel): 168 | data: Any 169 | `); 170 | }); 171 | 172 | it('correctly aliases pydantic reserved properties', async () => { 173 | const schema = buildSchema(/* GraphQL */ ` 174 | type AliasMe { 175 | copy: Int 176 | } 177 | `); 178 | 179 | const result = await plugin(schema, [], {}); 180 | 181 | expect(result).toBeSimilarStringTo(` 182 | from typing import Optional 183 | from pydantic import BaseModel, Field 184 | 185 | 186 | class AliasMe(BaseModel): 187 | copy_: Optional[int] = Field(None, alias='copy') 188 | `); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types", "test"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "es2018", 6 | "lib": ["es6", "esnext", "es2015", "dom"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "moduleResolution": "node", 21 | "baseUrl": "./", 22 | "paths": { 23 | "*": ["src/*", "node_modules/*"] 24 | }, 25 | "esModuleInterop": true 26 | } 27 | } --------------------------------------------------------------------------------