├── .circleci └── config.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc.js ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-interactive-tools.cjs └── releases │ └── yarn-3.6.3.cjs ├── .yarnrc.yml ├── README.markdown ├── bump.sh ├── integration ├── graphql-codegen-types-separate.yml ├── graphql-codegen-with-enum-mapping.yml ├── graphql-codegen.yml ├── graphql-type-factories.ts ├── graphql-types-and-factories-with-enum-mapping.ts ├── graphql-types-and-factories.ts ├── graphql-types-only.ts ├── integration.test.ts ├── schema.graphql ├── testData.ts └── tsconfig.json ├── jest.config.js ├── package.json ├── src └── index.ts ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@5.1.0 5 | 6 | node-image: &node-image 7 | image: cimg/node:20.9.0 8 | auth: 9 | username: $DOCKERHUB_USERNAME 10 | password: $DOCKERHUB_ACCESS_TOKEN 11 | 12 | workflows: 13 | version: 2 14 | workflow: 15 | jobs: 16 | - build: 17 | context: 18 | - npm-readonly 19 | - dockerhub 20 | - publish: 21 | context: 22 | - npm-publish 23 | - github 24 | - dockerhub 25 | requires: 26 | - build 27 | filters: 28 | branches: 29 | only: 30 | - main 31 | 32 | commands: 33 | bootstrap: 34 | steps: 35 | - checkout 36 | - run: 'echo "npmAuthToken: $NPM_TOKEN" >> ${HOME}/.yarnrc.yml' 37 | - node/install-packages: 38 | pkg-manager: yarn-berry 39 | with-cache: true 40 | include-branch-in-cache-key: false 41 | 42 | jobs: 43 | build: 44 | docker: 45 | - <<: *node-image 46 | working_directory: ~/project 47 | steps: 48 | - bootstrap 49 | - run: yarn build 50 | - run: yarn jest 51 | 52 | publish: 53 | docker: 54 | - <<: *node-image 55 | working_directory: ~/project 56 | steps: 57 | - bootstrap 58 | - run: yarn build 59 | - run: ./bump.sh package.json 60 | - run: yarn npm publish 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /build/ 3 | /node_modules/ 4 | 5 | # Yarn 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/sdks 12 | !.yarn/versions 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /integration 2 | /src 3 | /.idea 4 | /.circleci 5 | /jest.config.js 6 | /package.json.bak 7 | /bump.sh 8 | /yarn.lock 9 | /.yarnrc.yml 10 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "all", 3 | printWidth: 120, 4 | }; 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | npmPublishAccess: public 4 | 5 | plugins: 6 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 7 | spec: "@yarnpkg/plugin-interactive-tools" 8 | 9 | yarnPath: .yarn/releases/yarn-3.6.3.cjs 10 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | [![NPM](https://img.shields.io/npm/v/@homebound/graphql-typescript-factories)](https://www.npmjs.com/package/@homebound/graphql-typescript-factories) 2 | 3 | # graphql-typescript-factories 4 | 5 | This project is a plugin for [graphql-code-generator](http://www.graphql-code-generator.com) that generates `new` factory methods for use in client-side GraphQL tests that are stubbing/mocking out GraphQL responses. 6 | 7 | I.e. for a given schema like: 8 | 9 | ```graphql 10 | type Author { 11 | name: String! 12 | summary: AuthorSummary! 13 | popularity: Popularity! 14 | working: Working 15 | birthday: Date 16 | } 17 | 18 | # A DTO that is just some fields 19 | type AuthorSummary { 20 | author: Author! 21 | numberOfBooks: Int! 22 | amountOfSales: Float 23 | } 24 | ``` 25 | 26 | You'll get a factory method (generated in your regular `graphql-types.ts` output file) that let's you "one liner" create an `Author` with sane defaults: 27 | 28 | ```typescript 29 | const author = newAuthor(); 30 | 31 | expect(author.__typename).toEqual("Author") 32 | expect(author.name).toEqual("name") 33 | expect(author.summary.author).toStrictEqual(author); 34 | expect(author.summary.numberOfBooks).toEqual(0); 35 | ``` 36 | 37 | You can also override properties that are specific to your use case: 38 | 39 | ```typescript 40 | const author = newAuthor({ name: "long name" }); 41 | 42 | expect(author.__typename).toEqual("Author") 43 | expect(author.name).toEqual("name") 44 | ``` 45 | 46 | ## Install 47 | 48 | ```shell 49 | npm -i @homebound/graphql-typescript-factories 50 | ``` 51 | 52 | ### Use 53 | 54 | Since this is a plugin, make sure you have `graphql-codegen` installed prior to using this package 55 | 56 | Include the plugin in your `graphql-codegen.yml` config file: 57 | 58 | ```yaml 59 | overwrite: true 60 | schema: ./schema.json 61 | generates: 62 | src/generated/graphql-types.tsx: 63 | config: 64 | withHOC: false 65 | withHooks: true 66 | avoidOptionals: true 67 | plugins: 68 | - typescript 69 | - "@homebound/graphql-typescript-factories" 70 | ``` 71 | 72 | **Note**: The factories created by this plugin use the `Author`/etc. types generated by the regular `typescript` plugin, so you should have that included too. 73 | 74 | ### Putting Factories in a Separate File 75 | 76 | The above configuration will include the factories in the same `graphql-types.tsx` file as the regular `typescript` types. 77 | 78 | If you want to have the factories in a separate file, you can add a new output file, i.e. `graphql-factories.tsx`, specifically for this `graphql-typescript-factories` plugin, and set the `typesFilePath`, i.e. files: 79 | 80 | ```yaml 81 | overwrite: true 82 | schema: ./schema.json 83 | generates: 84 | src/generated/graphql-types.tsx: 85 | config: 86 | withHOC: false 87 | withHooks: true 88 | avoidOptionals: true 89 | plugins: 90 | - typescript 91 | src/generated/graphql-factories.tsx: 92 | config: 93 | withHOC: false 94 | withHooks: true 95 | avoidOptionals: true 96 | typesFilePath: "./graphql-types" 97 | plugins: 98 | - "@homebound/graphql-typescript-factories" 99 | ``` 100 | 101 | **Things to note:** 102 | 103 | - If your project requires extensionless imports, please leave out the `.ts(x)` extensions out of `typesFilePath`. Otherwise, for projects that want the file extensions, i.e. to be fully ESM compliant, please keep the file extensions in the path. 104 | 105 | ## Enum Details Pattern 106 | 107 | Somewhat tangentially, we've added first-class handling of our Homebound-specific "Enum Detail" pattern, where instead of returning enum values directly, we wrap the enum with a detail object that adds the string name, so that the client doesn't have to have its own boilerplate "enum to name" mapping. I.e. this schema: 108 | 109 | ```graphql 110 | enum EmployeeStatus { FULL_TIME, PART_TIME } 111 | 112 | type EmployeeStatusDetail { 113 | code: EmployeeStatus! 114 | name: String! 115 | } 116 | 117 | type Employee { 118 | status: EmployeeStatusDetail 119 | } 120 | ``` 121 | 122 | Allows a client to use the "enum detail" object to get other information than just the code. 123 | 124 | (Currently we only support an additional `name` field, but the intent would be to add more business-logic-y things to the detail object that is information the client might need, without having to hard-code `switch` statements on the client-side). 125 | 126 | Currently, any GraphQL object that has exactly two fields, named `code` and `name`, is assumed to be an Enum Detail wrapper. 127 | 128 | Once these wrapper types are recognized, we add some syntax sugar that allows creating objects with the enum itself as a shortcut, i.e.: 129 | 130 | ```typescript 131 | const employee = newEmployee({ 132 | status: EmployeeStatus.FULL_TIME, 133 | }); 134 | ``` 135 | 136 | Will work even though `status` is technically a `EmployeeStatusDetail` object and not the `EmployeeStatus` enum directly. 137 | 138 | If this feature/pattern is problematic for users who don't use it, we can add a config flag to disable it. 139 | 140 | ## Contributing 141 | 142 | In order to develop changes for this package, follow these steps: 143 | 144 | 1. Make your desired changes in the [`src` directory](/src) 145 | 146 | 2. Adjust the example files under the [`integration` directory](/integration) to use your new feature. 147 | 148 | 3. Run `yarn build`, to create a build with your changes 149 | 150 | 4. Run `yarn graphql-codegen`, and verify the output 151 | 152 | ## Todo 153 | 154 | - Support "number of children" 155 | - Support customizations like "if building DAG, reuse same project vs. make new project teach time" 156 | - Support providing a level of the DAG N-levels away (i.e. "create project but use this for the task") 157 | 158 | ## License 159 | 160 | MIT 161 | -------------------------------------------------------------------------------- /bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generates a version number. 4 | # 5 | # Usage: 6 | # ./bump.sh package.json 7 | # 8 | 9 | file=$1 10 | pattern=$(grep -o "[[:digit:]]\+\.0\.0-bump" < "${file}") 11 | major=$(echo "${pattern}"| grep -o "^[[:digit:]]\+") 12 | minor=$(git log --first-parent --format=%H | wc -l) 13 | version="${major}.${minor}.0" 14 | sed -i.bak "s/${pattern}/${version}/" "${file}" 15 | -------------------------------------------------------------------------------- /integration/graphql-codegen-types-separate.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "./integration/schema.graphql" 3 | config: 4 | scalarDefaults: 5 | Date: "./testData#newDate" 6 | taggedIds: 7 | AuthorSummary: "summary" 8 | generates: 9 | integration/graphql-types-only.ts: 10 | plugins: 11 | - typescript 12 | - typescript-operations 13 | integration/graphql-type-factories.ts: 14 | config: 15 | typesFilePath: "./graphql-types-only" 16 | plugins: 17 | - ./build/index.js 18 | -------------------------------------------------------------------------------- /integration/graphql-codegen-with-enum-mapping.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "./integration/schema.graphql" 3 | generates: 4 | integration/graphql-types-and-factories-with-enum-mapping.ts: 5 | config: 6 | namingConvention: 7 | enumValues: keep 8 | scalarDefaults: 9 | Date: "./testData#newDate" 10 | taggedIds: 11 | AuthorSummary: "summary" 12 | plugins: 13 | - typescript 14 | - typescript-operations 15 | - ./build/index.js 16 | -------------------------------------------------------------------------------- /integration/graphql-codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "./integration/schema.graphql" 3 | generates: 4 | integration/graphql-types-and-factories.ts: 5 | config: 6 | scalarDefaults: 7 | Date: "./testData#newDate" 8 | taggedIds: 9 | AuthorSummary: "summary" 10 | plugins: 11 | - typescript 12 | - typescript-operations 13 | - ./build/index.js 14 | -------------------------------------------------------------------------------- /integration/graphql-type-factories.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Author, 3 | AuthorSummary, 4 | Book, 5 | BookReview, 6 | BookStatus, 7 | CalendarInterval, 8 | Child, 9 | Maybe, 10 | Named, 11 | Parent, 12 | Popularity, 13 | PopularityDetail, 14 | SaveAuthorResult, 15 | SearchResult, 16 | SearchResults, 17 | Working, 18 | WorkingDetail, 19 | } from "./graphql-types-only"; 20 | import { newDate } from "./testData"; 21 | 22 | const factories: Record = {}; 23 | type RequireTypename = Omit & Required>; 24 | export interface AuthorOptions { 25 | __typename?: "Author"; 26 | anotherId?: Author["anotherId"]; 27 | birthday?: Author["birthday"]; 28 | bookPopularities?: Array; 29 | books?: Array; 30 | id?: Author["id"]; 31 | name?: Author["name"]; 32 | popularity?: PopularityDetailOptions | Popularity; 33 | summary?: AuthorSummary | AuthorSummaryOptions; 34 | working?: Author["working"]; 35 | workingDetail?: Array; 36 | } 37 | 38 | export function newAuthor(options: AuthorOptions = {}, cache: Record = {}): Author { 39 | const o = (options.__typename ? options : cache["Author"] = {}) as Author; 40 | (cache.all ??= new Set()).add(o); 41 | o.__typename = "Author"; 42 | o.anotherId = options.anotherId ?? "anotherId"; 43 | o.birthday = options.birthday ?? null; 44 | o.bookPopularities = (options.bookPopularities ?? []).map((i) => enumOrDetailOfPopularity(i)); 45 | o.books = (options.books ?? []).map((i) => maybeNew("Book", i, cache, options.hasOwnProperty("books"))); 46 | o.id = options.id ?? nextFactoryId("Author"); 47 | o.name = options.name ?? "name"; 48 | o.popularity = enumOrDetailOfPopularity(options.popularity); 49 | o.summary = maybeNew("AuthorSummary", options.summary, cache, options.hasOwnProperty("summary")); 50 | o.working = options.working ?? null; 51 | o.workingDetail = (options.workingDetail ?? []).map((i) => enumOrDetailOfWorking(i)); 52 | return o; 53 | } 54 | 55 | factories["Author"] = newAuthor; 56 | 57 | export interface AuthorSummaryOptions { 58 | __typename?: "AuthorSummary"; 59 | amountOfSales?: AuthorSummary["amountOfSales"]; 60 | author?: Author | AuthorOptions; 61 | id?: AuthorSummary["id"]; 62 | numberOfBooks?: AuthorSummary["numberOfBooks"]; 63 | } 64 | 65 | export function newAuthorSummary(options: AuthorSummaryOptions = {}, cache: Record = {}): AuthorSummary { 66 | const o = (options.__typename ? options : cache["AuthorSummary"] = {}) as AuthorSummary; 67 | (cache.all ??= new Set()).add(o); 68 | o.__typename = "AuthorSummary"; 69 | o.amountOfSales = options.amountOfSales ?? null; 70 | o.author = maybeNew("Author", options.author, cache, options.hasOwnProperty("author")); 71 | o.id = options.id ?? nextFactoryId("AuthorSummary"); 72 | o.numberOfBooks = options.numberOfBooks ?? 0; 73 | return o; 74 | } 75 | 76 | factories["AuthorSummary"] = newAuthorSummary; 77 | 78 | export interface BookOptions { 79 | __typename?: "Book"; 80 | coauthor?: Author | AuthorOptions | null; 81 | name?: Book["name"]; 82 | popularity?: PopularityDetailOptions | Popularity | null; 83 | reviews?: Array> | null; 84 | status?: Book["status"]; 85 | } 86 | 87 | export function newBook(options: BookOptions = {}, cache: Record = {}): Book { 88 | const o = (options.__typename ? options : cache["Book"] = {}) as Book; 89 | (cache.all ??= new Set()).add(o); 90 | o.__typename = "Book"; 91 | o.coauthor = maybeNewOrNull("Author", options.coauthor, cache); 92 | o.name = options.name ?? "name"; 93 | o.popularity = enumOrDetailOrNullOfPopularity(options.popularity); 94 | o.reviews = (options.reviews ?? []).map((i) => maybeNewOrNull("BookReview", i, cache)); 95 | o.status = options.status ?? BookStatus.InProgress; 96 | return o; 97 | } 98 | 99 | factories["Book"] = newBook; 100 | 101 | export interface BookReviewOptions { 102 | __typename?: "BookReview"; 103 | rating?: BookReview["rating"]; 104 | } 105 | 106 | export function newBookReview(options: BookReviewOptions = {}, cache: Record = {}): BookReview { 107 | const o = (options.__typename ? options : cache["BookReview"] = {}) as BookReview; 108 | (cache.all ??= new Set()).add(o); 109 | o.__typename = "BookReview"; 110 | o.rating = options.rating ?? 0; 111 | return o; 112 | } 113 | 114 | factories["BookReview"] = newBookReview; 115 | 116 | export interface CalendarIntervalOptions { 117 | __typename?: "CalendarInterval"; 118 | end?: CalendarInterval["end"]; 119 | start?: CalendarInterval["start"]; 120 | } 121 | 122 | export function newCalendarInterval( 123 | options: CalendarIntervalOptions = {}, 124 | cache: Record = {}, 125 | ): CalendarInterval { 126 | const o = (options.__typename ? options : cache["CalendarInterval"] = {}) as CalendarInterval; 127 | (cache.all ??= new Set()).add(o); 128 | o.__typename = "CalendarInterval"; 129 | o.end = options.end ?? newDate(); 130 | o.start = options.start ?? newDate(); 131 | return o; 132 | } 133 | 134 | factories["CalendarInterval"] = newCalendarInterval; 135 | 136 | export interface ChildOptions { 137 | __typename?: "Child"; 138 | name?: Child["name"]; 139 | parent?: Author | Book | Child | Parent | PopularityDetail | Named | NamedOptions; 140 | } 141 | 142 | export function newChild(options: ChildOptions = {}, cache: Record = {}): Child { 143 | const o = (options.__typename ? options : cache["Child"] = {}) as Child; 144 | (cache.all ??= new Set()).add(o); 145 | o.__typename = "Child"; 146 | o.name = options.name ?? "name"; 147 | o.parent = maybeNew("Named", options.parent, cache, options.hasOwnProperty("parent")); 148 | return o; 149 | } 150 | 151 | factories["Child"] = newChild; 152 | 153 | export interface ParentOptions { 154 | __typename?: "Parent"; 155 | children?: Array; 156 | name?: Parent["name"]; 157 | } 158 | 159 | export function newParent(options: ParentOptions = {}, cache: Record = {}): Parent { 160 | const o = (options.__typename ? options : cache["Parent"] = {}) as Parent; 161 | (cache.all ??= new Set()).add(o); 162 | o.__typename = "Parent"; 163 | o.children = (options.children ?? []).map((i) => maybeNew("Named", i, cache, options.hasOwnProperty("children"))); 164 | o.name = options.name ?? "name"; 165 | return o; 166 | } 167 | 168 | factories["Parent"] = newParent; 169 | 170 | export interface PopularityDetailOptions { 171 | __typename?: "PopularityDetail"; 172 | code?: PopularityDetail["code"]; 173 | name?: PopularityDetail["name"]; 174 | } 175 | 176 | export function newPopularityDetail( 177 | options: PopularityDetailOptions = {}, 178 | cache: Record = {}, 179 | ): PopularityDetail { 180 | const o = (options.__typename ? options : cache["PopularityDetail"] = {}) as PopularityDetail; 181 | (cache.all ??= new Set()).add(o); 182 | o.__typename = "PopularityDetail"; 183 | o.code = options.code ?? Popularity.High; 184 | o.name = options.name ?? "High"; 185 | return o; 186 | } 187 | 188 | factories["PopularityDetail"] = newPopularityDetail; 189 | 190 | export interface SaveAuthorResultOptions { 191 | __typename?: "SaveAuthorResult"; 192 | author?: Author | AuthorOptions; 193 | } 194 | 195 | export function newSaveAuthorResult( 196 | options: SaveAuthorResultOptions = {}, 197 | cache: Record = {}, 198 | ): SaveAuthorResult { 199 | const o = (options.__typename ? options : cache["SaveAuthorResult"] = {}) as SaveAuthorResult; 200 | (cache.all ??= new Set()).add(o); 201 | o.__typename = "SaveAuthorResult"; 202 | o.author = maybeNew("Author", options.author, cache, options.hasOwnProperty("author")); 203 | return o; 204 | } 205 | 206 | factories["SaveAuthorResult"] = newSaveAuthorResult; 207 | 208 | export interface SearchResultsOptions { 209 | __typename?: "SearchResults"; 210 | result1?: SearchResult | AuthorOptions | RequireTypename | null; 211 | result2?: Author | Book | Child | Parent | PopularityDetail | Named | NamedOptions | null; 212 | result3?: Author | AuthorOptions | null; 213 | } 214 | 215 | export function newSearchResults(options: SearchResultsOptions = {}, cache: Record = {}): SearchResults { 216 | const o = (options.__typename ? options : cache["SearchResults"] = {}) as SearchResults; 217 | (cache.all ??= new Set()).add(o); 218 | o.__typename = "SearchResults"; 219 | o.result1 = maybeNewOrNull(options.result1?.__typename ?? "Author", options.result1, cache); 220 | o.result2 = maybeNewOrNull("Named", options.result2, cache); 221 | o.result3 = maybeNewOrNull("Author", options.result3, cache); 222 | return o; 223 | } 224 | 225 | factories["SearchResults"] = newSearchResults; 226 | 227 | export interface WorkingDetailOptions { 228 | __typename?: "WorkingDetail"; 229 | code?: WorkingDetail["code"]; 230 | extraField?: WorkingDetail["extraField"]; 231 | name?: WorkingDetail["name"]; 232 | } 233 | 234 | export function newWorkingDetail(options: WorkingDetailOptions = {}, cache: Record = {}): WorkingDetail { 235 | const o = (options.__typename ? options : cache["WorkingDetail"] = {}) as WorkingDetail; 236 | (cache.all ??= new Set()).add(o); 237 | o.__typename = "WorkingDetail"; 238 | o.code = options.code ?? Working.No; 239 | o.extraField = options.extraField ?? 0; 240 | o.name = options.name ?? "No"; 241 | return o; 242 | } 243 | 244 | factories["WorkingDetail"] = newWorkingDetail; 245 | 246 | export type NamedOptions = 247 | | AuthorOptions 248 | | RequireTypename 249 | | RequireTypename 250 | | RequireTypename 251 | | RequireTypename; 252 | 253 | export type NamedType = Author | Book | Child | Parent | PopularityDetail; 254 | 255 | export type NamedTypeName = "Author" | "Book" | "Child" | "Parent" | "PopularityDetail"; 256 | 257 | export function newNamed(): Author; 258 | export function newNamed(options: AuthorOptions, cache?: Record): Author; 259 | export function newNamed(options: RequireTypename, cache?: Record): Book; 260 | export function newNamed(options: RequireTypename, cache?: Record): Child; 261 | export function newNamed(options: RequireTypename, cache?: Record): Parent; 262 | export function newNamed( 263 | options: RequireTypename, 264 | cache?: Record, 265 | ): PopularityDetail; 266 | export function newNamed(options: NamedOptions = {}, cache: Record = {}): NamedType { 267 | const { __typename = "Author" } = options ?? {}; 268 | const maybeCached = Object.keys(options).length === 0 ? cache[__typename] : undefined; 269 | return maybeCached ?? maybeNew(__typename, options ?? {}, cache); 270 | } 271 | 272 | factories["Named"] = newNamed; 273 | 274 | const enumDetailNameOfPopularity = { High: "High", Low: "Low" }; 275 | 276 | function enumOrDetailOfPopularity(enumOrDetail: PopularityDetailOptions | Popularity | undefined): PopularityDetail { 277 | if (enumOrDetail === undefined) { 278 | return newPopularityDetail(); 279 | } else if (typeof enumOrDetail === "object" && "code" in enumOrDetail) { 280 | return { 281 | __typename: "PopularityDetail", 282 | code: enumOrDetail.code!, 283 | name: enumDetailNameOfPopularity[enumOrDetail.code!], 284 | ...enumOrDetail, 285 | } as PopularityDetail; 286 | } else { 287 | return newPopularityDetail({ 288 | code: enumOrDetail as Popularity, 289 | name: enumDetailNameOfPopularity[enumOrDetail as Popularity], 290 | }); 291 | } 292 | } 293 | 294 | function enumOrDetailOrNullOfPopularity( 295 | enumOrDetail: PopularityDetailOptions | Popularity | undefined | null, 296 | ): PopularityDetail | null { 297 | if (enumOrDetail === null) { 298 | return null; 299 | } 300 | return enumOrDetailOfPopularity(enumOrDetail); 301 | } 302 | 303 | const enumDetailNameOfWorking = { NO: "No", YES: "Yes" }; 304 | 305 | function enumOrDetailOfWorking(enumOrDetail: WorkingDetailOptions | Working | undefined): WorkingDetail { 306 | if (enumOrDetail === undefined) { 307 | return newWorkingDetail(); 308 | } else if (typeof enumOrDetail === "object" && "code" in enumOrDetail) { 309 | return { 310 | __typename: "WorkingDetail", 311 | code: enumOrDetail.code!, 312 | name: enumDetailNameOfWorking[enumOrDetail.code!], 313 | ...enumOrDetail, 314 | } as WorkingDetail; 315 | } else { 316 | return newWorkingDetail({ code: enumOrDetail as Working, name: enumDetailNameOfWorking[enumOrDetail as Working] }); 317 | } 318 | } 319 | 320 | function enumOrDetailOrNullOfWorking( 321 | enumOrDetail: WorkingDetailOptions | Working | undefined | null, 322 | ): WorkingDetail | null { 323 | if (enumOrDetail === null) { 324 | return null; 325 | } 326 | return enumOrDetailOfWorking(enumOrDetail); 327 | } 328 | 329 | const taggedIds: Record = { "AuthorSummary": "summary" }; 330 | let nextFactoryIds: Record = {}; 331 | 332 | export function resetFactoryIds() { 333 | nextFactoryIds = {}; 334 | } 335 | 336 | function nextFactoryId(objectName: string): string { 337 | const nextId = nextFactoryIds[objectName] || 1; 338 | nextFactoryIds[objectName] = nextId + 1; 339 | const tag = taggedIds[objectName] ?? objectName.replace(/[a-z]/g, "").toLowerCase(); 340 | return tag + ":" + nextId; 341 | } 342 | 343 | function maybeNew( 344 | type: string, 345 | value: { __typename?: string } | object | undefined, 346 | cache: Record, 347 | isSet: boolean = false, 348 | ): any { 349 | if (value === undefined) { 350 | return isSet ? undefined : cache[type] || factories[type]({}, cache); 351 | } else if ("__typename" in value && value.__typename) { 352 | return cache.all?.has(value) ? value : factories[value.__typename](value, cache); 353 | } else { 354 | return factories[type](value, cache); 355 | } 356 | } 357 | 358 | function maybeNewOrNull( 359 | type: string, 360 | value: { __typename?: string } | object | undefined | null, 361 | cache: Record, 362 | ): any { 363 | if (!value) { 364 | return null; 365 | } else if ("__typename" in value && value.__typename) { 366 | return cache.all?.has(value) ? value : factories[value.__typename](value, cache); 367 | } else { 368 | return factories[type](value, cache); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /integration/graphql-types-and-factories-with-enum-mapping.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type InputMaybe = Maybe; 3 | export type Exact = { [K in keyof T]: T[K] }; 4 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 5 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 6 | export type MakeEmpty = { [_ in K]?: never }; 7 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 8 | /** All built-in and custom scalars, mapped to their actual values */ 9 | export type Scalars = { 10 | ID: { input: string; output: string; } 11 | String: { input: string; output: string; } 12 | Boolean: { input: boolean; output: boolean; } 13 | Int: { input: number; output: number; } 14 | Float: { input: number; output: number; } 15 | Date: { input: any; output: any; } 16 | }; 17 | 18 | export type Author = Named & { 19 | __typename?: 'Author'; 20 | anotherId: Scalars['ID']['output']; 21 | birthday?: Maybe; 22 | bookPopularities: Array; 23 | books: Array; 24 | id: Scalars['ID']['output']; 25 | name: Scalars['String']['output']; 26 | popularity: PopularityDetail; 27 | summary: AuthorSummary; 28 | working?: Maybe; 29 | workingDetail: Array; 30 | }; 31 | 32 | export type AuthorInput = { 33 | name?: InputMaybe; 34 | }; 35 | 36 | export type AuthorSummary = { 37 | __typename?: 'AuthorSummary'; 38 | amountOfSales?: Maybe; 39 | author: Author; 40 | id: Scalars['ID']['output']; 41 | numberOfBooks: Scalars['Int']['output']; 42 | }; 43 | 44 | export type Book = Named & { 45 | __typename?: 'Book'; 46 | coauthor?: Maybe; 47 | name: Scalars['String']['output']; 48 | popularity?: Maybe; 49 | reviews?: Maybe>>; 50 | status: BookStatus; 51 | }; 52 | 53 | export type BookReview = { 54 | __typename?: 'BookReview'; 55 | rating: Scalars['Int']['output']; 56 | }; 57 | 58 | export enum BookStatus { 59 | IN_PROGRESS = 'IN_PROGRESS', 60 | NOT_STARTED = 'NOT_STARTED', 61 | ON_HOLD = 'ON_HOLD' 62 | } 63 | 64 | export type CalendarInterval = { 65 | __typename?: 'CalendarInterval'; 66 | end: Scalars['Date']['output']; 67 | start: Scalars['Date']['output']; 68 | }; 69 | 70 | export type Child = Named & { 71 | __typename?: 'Child'; 72 | name: Scalars['String']['output']; 73 | parent: Named; 74 | }; 75 | 76 | export type Mutation = { 77 | __typename?: 'Mutation'; 78 | saveAuthor: SaveAuthorResult; 79 | }; 80 | 81 | 82 | export type MutationSaveAuthorArgs = { 83 | input: AuthorInput; 84 | }; 85 | 86 | export type Named = { 87 | name: Scalars['String']['output']; 88 | }; 89 | 90 | export type Parent = Named & { 91 | __typename?: 'Parent'; 92 | children: Array; 93 | name: Scalars['String']['output']; 94 | }; 95 | 96 | export enum Popularity { 97 | High = 'High', 98 | Low = 'Low' 99 | } 100 | 101 | export type PopularityDetail = Named & { 102 | __typename?: 'PopularityDetail'; 103 | code: Popularity; 104 | name: Scalars['String']['output']; 105 | }; 106 | 107 | export type Query = { 108 | __typename?: 'Query'; 109 | authorSummaries: Array; 110 | authors: Array; 111 | search: Array; 112 | }; 113 | 114 | 115 | export type QueryAuthorsArgs = { 116 | id?: InputMaybe; 117 | }; 118 | 119 | 120 | export type QuerySearchArgs = { 121 | query: Scalars['String']['input']; 122 | }; 123 | 124 | export type SaveAuthorResult = { 125 | __typename?: 'SaveAuthorResult'; 126 | author: Author; 127 | }; 128 | 129 | export type SearchResult = Author | Book; 130 | 131 | export type SearchResults = { 132 | __typename?: 'SearchResults'; 133 | result1?: Maybe; 134 | result2?: Maybe; 135 | result3?: Maybe; 136 | }; 137 | 138 | export enum Working { 139 | NO = 'NO', 140 | YES = 'YES' 141 | } 142 | 143 | export type WorkingDetail = { 144 | __typename?: 'WorkingDetail'; 145 | code: Working; 146 | extraField: Scalars['Int']['output']; 147 | name: Scalars['String']['output']; 148 | }; 149 | 150 | import { newDate } from "./testData"; 151 | 152 | const factories: Record = {}; 153 | type RequireTypename = Omit & Required>; 154 | export interface AuthorOptions { 155 | __typename?: "Author"; 156 | anotherId?: Author["anotherId"]; 157 | birthday?: Author["birthday"]; 158 | bookPopularities?: Array; 159 | books?: Array; 160 | id?: Author["id"]; 161 | name?: Author["name"]; 162 | popularity?: PopularityDetailOptions | Popularity; 163 | summary?: AuthorSummary | AuthorSummaryOptions; 164 | working?: Author["working"]; 165 | workingDetail?: Array; 166 | } 167 | 168 | export function newAuthor(options: AuthorOptions = {}, cache: Record = {}): Author { 169 | const o = (options.__typename ? options : cache["Author"] = {}) as Author; 170 | (cache.all ??= new Set()).add(o); 171 | o.__typename = "Author"; 172 | o.anotherId = options.anotherId ?? "anotherId"; 173 | o.birthday = options.birthday ?? null; 174 | o.bookPopularities = (options.bookPopularities ?? []).map((i) => enumOrDetailOfPopularity(i)); 175 | o.books = (options.books ?? []).map((i) => maybeNew("Book", i, cache, options.hasOwnProperty("books"))); 176 | o.id = options.id ?? nextFactoryId("Author"); 177 | o.name = options.name ?? "name"; 178 | o.popularity = enumOrDetailOfPopularity(options.popularity); 179 | o.summary = maybeNew("AuthorSummary", options.summary, cache, options.hasOwnProperty("summary")); 180 | o.working = options.working ?? null; 181 | o.workingDetail = (options.workingDetail ?? []).map((i) => enumOrDetailOfWorking(i)); 182 | return o; 183 | } 184 | 185 | factories["Author"] = newAuthor; 186 | 187 | export interface AuthorSummaryOptions { 188 | __typename?: "AuthorSummary"; 189 | amountOfSales?: AuthorSummary["amountOfSales"]; 190 | author?: Author | AuthorOptions; 191 | id?: AuthorSummary["id"]; 192 | numberOfBooks?: AuthorSummary["numberOfBooks"]; 193 | } 194 | 195 | export function newAuthorSummary(options: AuthorSummaryOptions = {}, cache: Record = {}): AuthorSummary { 196 | const o = (options.__typename ? options : cache["AuthorSummary"] = {}) as AuthorSummary; 197 | (cache.all ??= new Set()).add(o); 198 | o.__typename = "AuthorSummary"; 199 | o.amountOfSales = options.amountOfSales ?? null; 200 | o.author = maybeNew("Author", options.author, cache, options.hasOwnProperty("author")); 201 | o.id = options.id ?? nextFactoryId("AuthorSummary"); 202 | o.numberOfBooks = options.numberOfBooks ?? 0; 203 | return o; 204 | } 205 | 206 | factories["AuthorSummary"] = newAuthorSummary; 207 | 208 | export interface BookOptions { 209 | __typename?: "Book"; 210 | coauthor?: Author | AuthorOptions | null; 211 | name?: Book["name"]; 212 | popularity?: PopularityDetailOptions | Popularity | null; 213 | reviews?: Array> | null; 214 | status?: Book["status"]; 215 | } 216 | 217 | export function newBook(options: BookOptions = {}, cache: Record = {}): Book { 218 | const o = (options.__typename ? options : cache["Book"] = {}) as Book; 219 | (cache.all ??= new Set()).add(o); 220 | o.__typename = "Book"; 221 | o.coauthor = maybeNewOrNull("Author", options.coauthor, cache); 222 | o.name = options.name ?? "name"; 223 | o.popularity = enumOrDetailOrNullOfPopularity(options.popularity); 224 | o.reviews = (options.reviews ?? []).map((i) => maybeNewOrNull("BookReview", i, cache)); 225 | o.status = options.status ?? BookStatus.IN_PROGRESS; 226 | return o; 227 | } 228 | 229 | factories["Book"] = newBook; 230 | 231 | export interface BookReviewOptions { 232 | __typename?: "BookReview"; 233 | rating?: BookReview["rating"]; 234 | } 235 | 236 | export function newBookReview(options: BookReviewOptions = {}, cache: Record = {}): BookReview { 237 | const o = (options.__typename ? options : cache["BookReview"] = {}) as BookReview; 238 | (cache.all ??= new Set()).add(o); 239 | o.__typename = "BookReview"; 240 | o.rating = options.rating ?? 0; 241 | return o; 242 | } 243 | 244 | factories["BookReview"] = newBookReview; 245 | 246 | export interface CalendarIntervalOptions { 247 | __typename?: "CalendarInterval"; 248 | end?: CalendarInterval["end"]; 249 | start?: CalendarInterval["start"]; 250 | } 251 | 252 | export function newCalendarInterval( 253 | options: CalendarIntervalOptions = {}, 254 | cache: Record = {}, 255 | ): CalendarInterval { 256 | const o = (options.__typename ? options : cache["CalendarInterval"] = {}) as CalendarInterval; 257 | (cache.all ??= new Set()).add(o); 258 | o.__typename = "CalendarInterval"; 259 | o.end = options.end ?? newDate(); 260 | o.start = options.start ?? newDate(); 261 | return o; 262 | } 263 | 264 | factories["CalendarInterval"] = newCalendarInterval; 265 | 266 | export interface ChildOptions { 267 | __typename?: "Child"; 268 | name?: Child["name"]; 269 | parent?: Author | Book | Child | Parent | PopularityDetail | Named | NamedOptions; 270 | } 271 | 272 | export function newChild(options: ChildOptions = {}, cache: Record = {}): Child { 273 | const o = (options.__typename ? options : cache["Child"] = {}) as Child; 274 | (cache.all ??= new Set()).add(o); 275 | o.__typename = "Child"; 276 | o.name = options.name ?? "name"; 277 | o.parent = maybeNew("Named", options.parent, cache, options.hasOwnProperty("parent")); 278 | return o; 279 | } 280 | 281 | factories["Child"] = newChild; 282 | 283 | export interface ParentOptions { 284 | __typename?: "Parent"; 285 | children?: Array; 286 | name?: Parent["name"]; 287 | } 288 | 289 | export function newParent(options: ParentOptions = {}, cache: Record = {}): Parent { 290 | const o = (options.__typename ? options : cache["Parent"] = {}) as Parent; 291 | (cache.all ??= new Set()).add(o); 292 | o.__typename = "Parent"; 293 | o.children = (options.children ?? []).map((i) => maybeNew("Named", i, cache, options.hasOwnProperty("children"))); 294 | o.name = options.name ?? "name"; 295 | return o; 296 | } 297 | 298 | factories["Parent"] = newParent; 299 | 300 | export interface PopularityDetailOptions { 301 | __typename?: "PopularityDetail"; 302 | code?: PopularityDetail["code"]; 303 | name?: PopularityDetail["name"]; 304 | } 305 | 306 | export function newPopularityDetail( 307 | options: PopularityDetailOptions = {}, 308 | cache: Record = {}, 309 | ): PopularityDetail { 310 | const o = (options.__typename ? options : cache["PopularityDetail"] = {}) as PopularityDetail; 311 | (cache.all ??= new Set()).add(o); 312 | o.__typename = "PopularityDetail"; 313 | o.code = options.code ?? Popularity.High; 314 | o.name = options.name ?? "High"; 315 | return o; 316 | } 317 | 318 | factories["PopularityDetail"] = newPopularityDetail; 319 | 320 | export interface SaveAuthorResultOptions { 321 | __typename?: "SaveAuthorResult"; 322 | author?: Author | AuthorOptions; 323 | } 324 | 325 | export function newSaveAuthorResult( 326 | options: SaveAuthorResultOptions = {}, 327 | cache: Record = {}, 328 | ): SaveAuthorResult { 329 | const o = (options.__typename ? options : cache["SaveAuthorResult"] = {}) as SaveAuthorResult; 330 | (cache.all ??= new Set()).add(o); 331 | o.__typename = "SaveAuthorResult"; 332 | o.author = maybeNew("Author", options.author, cache, options.hasOwnProperty("author")); 333 | return o; 334 | } 335 | 336 | factories["SaveAuthorResult"] = newSaveAuthorResult; 337 | 338 | export interface SearchResultsOptions { 339 | __typename?: "SearchResults"; 340 | result1?: SearchResult | AuthorOptions | RequireTypename | null; 341 | result2?: Author | Book | Child | Parent | PopularityDetail | Named | NamedOptions | null; 342 | result3?: Author | AuthorOptions | null; 343 | } 344 | 345 | export function newSearchResults(options: SearchResultsOptions = {}, cache: Record = {}): SearchResults { 346 | const o = (options.__typename ? options : cache["SearchResults"] = {}) as SearchResults; 347 | (cache.all ??= new Set()).add(o); 348 | o.__typename = "SearchResults"; 349 | o.result1 = maybeNewOrNull(options.result1?.__typename ?? "Author", options.result1, cache); 350 | o.result2 = maybeNewOrNull("Named", options.result2, cache); 351 | o.result3 = maybeNewOrNull("Author", options.result3, cache); 352 | return o; 353 | } 354 | 355 | factories["SearchResults"] = newSearchResults; 356 | 357 | export interface WorkingDetailOptions { 358 | __typename?: "WorkingDetail"; 359 | code?: WorkingDetail["code"]; 360 | extraField?: WorkingDetail["extraField"]; 361 | name?: WorkingDetail["name"]; 362 | } 363 | 364 | export function newWorkingDetail(options: WorkingDetailOptions = {}, cache: Record = {}): WorkingDetail { 365 | const o = (options.__typename ? options : cache["WorkingDetail"] = {}) as WorkingDetail; 366 | (cache.all ??= new Set()).add(o); 367 | o.__typename = "WorkingDetail"; 368 | o.code = options.code ?? Working.NO; 369 | o.extraField = options.extraField ?? 0; 370 | o.name = options.name ?? "No"; 371 | return o; 372 | } 373 | 374 | factories["WorkingDetail"] = newWorkingDetail; 375 | 376 | export type NamedOptions = 377 | | AuthorOptions 378 | | RequireTypename 379 | | RequireTypename 380 | | RequireTypename 381 | | RequireTypename; 382 | 383 | export type NamedType = Author | Book | Child | Parent | PopularityDetail; 384 | 385 | export type NamedTypeName = "Author" | "Book" | "Child" | "Parent" | "PopularityDetail"; 386 | 387 | export function newNamed(): Author; 388 | export function newNamed(options: AuthorOptions, cache?: Record): Author; 389 | export function newNamed(options: RequireTypename, cache?: Record): Book; 390 | export function newNamed(options: RequireTypename, cache?: Record): Child; 391 | export function newNamed(options: RequireTypename, cache?: Record): Parent; 392 | export function newNamed( 393 | options: RequireTypename, 394 | cache?: Record, 395 | ): PopularityDetail; 396 | export function newNamed(options: NamedOptions = {}, cache: Record = {}): NamedType { 397 | const { __typename = "Author" } = options ?? {}; 398 | const maybeCached = Object.keys(options).length === 0 ? cache[__typename] : undefined; 399 | return maybeCached ?? maybeNew(__typename, options ?? {}, cache); 400 | } 401 | 402 | factories["Named"] = newNamed; 403 | 404 | const enumDetailNameOfPopularity = { High: "High", Low: "Low" }; 405 | 406 | function enumOrDetailOfPopularity(enumOrDetail: PopularityDetailOptions | Popularity | undefined): PopularityDetail { 407 | if (enumOrDetail === undefined) { 408 | return newPopularityDetail(); 409 | } else if (typeof enumOrDetail === "object" && "code" in enumOrDetail) { 410 | return { 411 | __typename: "PopularityDetail", 412 | code: enumOrDetail.code!, 413 | name: enumDetailNameOfPopularity[enumOrDetail.code!], 414 | ...enumOrDetail, 415 | } as PopularityDetail; 416 | } else { 417 | return newPopularityDetail({ 418 | code: enumOrDetail as Popularity, 419 | name: enumDetailNameOfPopularity[enumOrDetail as Popularity], 420 | }); 421 | } 422 | } 423 | 424 | function enumOrDetailOrNullOfPopularity( 425 | enumOrDetail: PopularityDetailOptions | Popularity | undefined | null, 426 | ): PopularityDetail | null { 427 | if (enumOrDetail === null) { 428 | return null; 429 | } 430 | return enumOrDetailOfPopularity(enumOrDetail); 431 | } 432 | 433 | const enumDetailNameOfWorking = { NO: "No", YES: "Yes" }; 434 | 435 | function enumOrDetailOfWorking(enumOrDetail: WorkingDetailOptions | Working | undefined): WorkingDetail { 436 | if (enumOrDetail === undefined) { 437 | return newWorkingDetail(); 438 | } else if (typeof enumOrDetail === "object" && "code" in enumOrDetail) { 439 | return { 440 | __typename: "WorkingDetail", 441 | code: enumOrDetail.code!, 442 | name: enumDetailNameOfWorking[enumOrDetail.code!], 443 | ...enumOrDetail, 444 | } as WorkingDetail; 445 | } else { 446 | return newWorkingDetail({ code: enumOrDetail as Working, name: enumDetailNameOfWorking[enumOrDetail as Working] }); 447 | } 448 | } 449 | 450 | function enumOrDetailOrNullOfWorking( 451 | enumOrDetail: WorkingDetailOptions | Working | undefined | null, 452 | ): WorkingDetail | null { 453 | if (enumOrDetail === null) { 454 | return null; 455 | } 456 | return enumOrDetailOfWorking(enumOrDetail); 457 | } 458 | 459 | const taggedIds: Record = { "AuthorSummary": "summary" }; 460 | let nextFactoryIds: Record = {}; 461 | 462 | export function resetFactoryIds() { 463 | nextFactoryIds = {}; 464 | } 465 | 466 | function nextFactoryId(objectName: string): string { 467 | const nextId = nextFactoryIds[objectName] || 1; 468 | nextFactoryIds[objectName] = nextId + 1; 469 | const tag = taggedIds[objectName] ?? objectName.replace(/[a-z]/g, "").toLowerCase(); 470 | return tag + ":" + nextId; 471 | } 472 | 473 | function maybeNew( 474 | type: string, 475 | value: { __typename?: string } | object | undefined, 476 | cache: Record, 477 | isSet: boolean = false, 478 | ): any { 479 | if (value === undefined) { 480 | return isSet ? undefined : cache[type] || factories[type]({}, cache); 481 | } else if ("__typename" in value && value.__typename) { 482 | return cache.all?.has(value) ? value : factories[value.__typename](value, cache); 483 | } else { 484 | return factories[type](value, cache); 485 | } 486 | } 487 | 488 | function maybeNewOrNull( 489 | type: string, 490 | value: { __typename?: string } | object | undefined | null, 491 | cache: Record, 492 | ): any { 493 | if (!value) { 494 | return null; 495 | } else if ("__typename" in value && value.__typename) { 496 | return cache.all?.has(value) ? value : factories[value.__typename](value, cache); 497 | } else { 498 | return factories[type](value, cache); 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /integration/graphql-types-and-factories.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type InputMaybe = Maybe; 3 | export type Exact = { [K in keyof T]: T[K] }; 4 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 5 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 6 | export type MakeEmpty = { [_ in K]?: never }; 7 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 8 | /** All built-in and custom scalars, mapped to their actual values */ 9 | export type Scalars = { 10 | ID: { input: string; output: string; } 11 | String: { input: string; output: string; } 12 | Boolean: { input: boolean; output: boolean; } 13 | Int: { input: number; output: number; } 14 | Float: { input: number; output: number; } 15 | Date: { input: any; output: any; } 16 | }; 17 | 18 | export type Author = Named & { 19 | __typename?: 'Author'; 20 | anotherId: Scalars['ID']['output']; 21 | birthday?: Maybe; 22 | bookPopularities: Array; 23 | books: Array; 24 | id: Scalars['ID']['output']; 25 | name: Scalars['String']['output']; 26 | popularity: PopularityDetail; 27 | summary: AuthorSummary; 28 | working?: Maybe; 29 | workingDetail: Array; 30 | }; 31 | 32 | export type AuthorInput = { 33 | name?: InputMaybe; 34 | }; 35 | 36 | export type AuthorSummary = { 37 | __typename?: 'AuthorSummary'; 38 | amountOfSales?: Maybe; 39 | author: Author; 40 | id: Scalars['ID']['output']; 41 | numberOfBooks: Scalars['Int']['output']; 42 | }; 43 | 44 | export type Book = Named & { 45 | __typename?: 'Book'; 46 | coauthor?: Maybe; 47 | name: Scalars['String']['output']; 48 | popularity?: Maybe; 49 | reviews?: Maybe>>; 50 | status: BookStatus; 51 | }; 52 | 53 | export type BookReview = { 54 | __typename?: 'BookReview'; 55 | rating: Scalars['Int']['output']; 56 | }; 57 | 58 | export enum BookStatus { 59 | InProgress = 'IN_PROGRESS', 60 | NotStarted = 'NOT_STARTED', 61 | OnHold = 'ON_HOLD' 62 | } 63 | 64 | export type CalendarInterval = { 65 | __typename?: 'CalendarInterval'; 66 | end: Scalars['Date']['output']; 67 | start: Scalars['Date']['output']; 68 | }; 69 | 70 | export type Child = Named & { 71 | __typename?: 'Child'; 72 | name: Scalars['String']['output']; 73 | parent: Named; 74 | }; 75 | 76 | export type Mutation = { 77 | __typename?: 'Mutation'; 78 | saveAuthor: SaveAuthorResult; 79 | }; 80 | 81 | 82 | export type MutationSaveAuthorArgs = { 83 | input: AuthorInput; 84 | }; 85 | 86 | export type Named = { 87 | name: Scalars['String']['output']; 88 | }; 89 | 90 | export type Parent = Named & { 91 | __typename?: 'Parent'; 92 | children: Array; 93 | name: Scalars['String']['output']; 94 | }; 95 | 96 | export enum Popularity { 97 | High = 'High', 98 | Low = 'Low' 99 | } 100 | 101 | export type PopularityDetail = Named & { 102 | __typename?: 'PopularityDetail'; 103 | code: Popularity; 104 | name: Scalars['String']['output']; 105 | }; 106 | 107 | export type Query = { 108 | __typename?: 'Query'; 109 | authorSummaries: Array; 110 | authors: Array; 111 | search: Array; 112 | }; 113 | 114 | 115 | export type QueryAuthorsArgs = { 116 | id?: InputMaybe; 117 | }; 118 | 119 | 120 | export type QuerySearchArgs = { 121 | query: Scalars['String']['input']; 122 | }; 123 | 124 | export type SaveAuthorResult = { 125 | __typename?: 'SaveAuthorResult'; 126 | author: Author; 127 | }; 128 | 129 | export type SearchResult = Author | Book; 130 | 131 | export type SearchResults = { 132 | __typename?: 'SearchResults'; 133 | result1?: Maybe; 134 | result2?: Maybe; 135 | result3?: Maybe; 136 | }; 137 | 138 | export enum Working { 139 | No = 'NO', 140 | Yes = 'YES' 141 | } 142 | 143 | export type WorkingDetail = { 144 | __typename?: 'WorkingDetail'; 145 | code: Working; 146 | extraField: Scalars['Int']['output']; 147 | name: Scalars['String']['output']; 148 | }; 149 | 150 | import { newDate } from "./testData"; 151 | 152 | const factories: Record = {}; 153 | type RequireTypename = Omit & Required>; 154 | export interface AuthorOptions { 155 | __typename?: "Author"; 156 | anotherId?: Author["anotherId"]; 157 | birthday?: Author["birthday"]; 158 | bookPopularities?: Array; 159 | books?: Array; 160 | id?: Author["id"]; 161 | name?: Author["name"]; 162 | popularity?: PopularityDetailOptions | Popularity; 163 | summary?: AuthorSummary | AuthorSummaryOptions; 164 | working?: Author["working"]; 165 | workingDetail?: Array; 166 | } 167 | 168 | export function newAuthor(options: AuthorOptions = {}, cache: Record = {}): Author { 169 | const o = (options.__typename ? options : cache["Author"] = {}) as Author; 170 | (cache.all ??= new Set()).add(o); 171 | o.__typename = "Author"; 172 | o.anotherId = options.anotherId ?? "anotherId"; 173 | o.birthday = options.birthday ?? null; 174 | o.bookPopularities = (options.bookPopularities ?? []).map((i) => enumOrDetailOfPopularity(i)); 175 | o.books = (options.books ?? []).map((i) => maybeNew("Book", i, cache, options.hasOwnProperty("books"))); 176 | o.id = options.id ?? nextFactoryId("Author"); 177 | o.name = options.name ?? "name"; 178 | o.popularity = enumOrDetailOfPopularity(options.popularity); 179 | o.summary = maybeNew("AuthorSummary", options.summary, cache, options.hasOwnProperty("summary")); 180 | o.working = options.working ?? null; 181 | o.workingDetail = (options.workingDetail ?? []).map((i) => enumOrDetailOfWorking(i)); 182 | return o; 183 | } 184 | 185 | factories["Author"] = newAuthor; 186 | 187 | export interface AuthorSummaryOptions { 188 | __typename?: "AuthorSummary"; 189 | amountOfSales?: AuthorSummary["amountOfSales"]; 190 | author?: Author | AuthorOptions; 191 | id?: AuthorSummary["id"]; 192 | numberOfBooks?: AuthorSummary["numberOfBooks"]; 193 | } 194 | 195 | export function newAuthorSummary(options: AuthorSummaryOptions = {}, cache: Record = {}): AuthorSummary { 196 | const o = (options.__typename ? options : cache["AuthorSummary"] = {}) as AuthorSummary; 197 | (cache.all ??= new Set()).add(o); 198 | o.__typename = "AuthorSummary"; 199 | o.amountOfSales = options.amountOfSales ?? null; 200 | o.author = maybeNew("Author", options.author, cache, options.hasOwnProperty("author")); 201 | o.id = options.id ?? nextFactoryId("AuthorSummary"); 202 | o.numberOfBooks = options.numberOfBooks ?? 0; 203 | return o; 204 | } 205 | 206 | factories["AuthorSummary"] = newAuthorSummary; 207 | 208 | export interface BookOptions { 209 | __typename?: "Book"; 210 | coauthor?: Author | AuthorOptions | null; 211 | name?: Book["name"]; 212 | popularity?: PopularityDetailOptions | Popularity | null; 213 | reviews?: Array> | null; 214 | status?: Book["status"]; 215 | } 216 | 217 | export function newBook(options: BookOptions = {}, cache: Record = {}): Book { 218 | const o = (options.__typename ? options : cache["Book"] = {}) as Book; 219 | (cache.all ??= new Set()).add(o); 220 | o.__typename = "Book"; 221 | o.coauthor = maybeNewOrNull("Author", options.coauthor, cache); 222 | o.name = options.name ?? "name"; 223 | o.popularity = enumOrDetailOrNullOfPopularity(options.popularity); 224 | o.reviews = (options.reviews ?? []).map((i) => maybeNewOrNull("BookReview", i, cache)); 225 | o.status = options.status ?? BookStatus.InProgress; 226 | return o; 227 | } 228 | 229 | factories["Book"] = newBook; 230 | 231 | export interface BookReviewOptions { 232 | __typename?: "BookReview"; 233 | rating?: BookReview["rating"]; 234 | } 235 | 236 | export function newBookReview(options: BookReviewOptions = {}, cache: Record = {}): BookReview { 237 | const o = (options.__typename ? options : cache["BookReview"] = {}) as BookReview; 238 | (cache.all ??= new Set()).add(o); 239 | o.__typename = "BookReview"; 240 | o.rating = options.rating ?? 0; 241 | return o; 242 | } 243 | 244 | factories["BookReview"] = newBookReview; 245 | 246 | export interface CalendarIntervalOptions { 247 | __typename?: "CalendarInterval"; 248 | end?: CalendarInterval["end"]; 249 | start?: CalendarInterval["start"]; 250 | } 251 | 252 | export function newCalendarInterval( 253 | options: CalendarIntervalOptions = {}, 254 | cache: Record = {}, 255 | ): CalendarInterval { 256 | const o = (options.__typename ? options : cache["CalendarInterval"] = {}) as CalendarInterval; 257 | (cache.all ??= new Set()).add(o); 258 | o.__typename = "CalendarInterval"; 259 | o.end = options.end ?? newDate(); 260 | o.start = options.start ?? newDate(); 261 | return o; 262 | } 263 | 264 | factories["CalendarInterval"] = newCalendarInterval; 265 | 266 | export interface ChildOptions { 267 | __typename?: "Child"; 268 | name?: Child["name"]; 269 | parent?: Author | Book | Child | Parent | PopularityDetail | Named | NamedOptions; 270 | } 271 | 272 | export function newChild(options: ChildOptions = {}, cache: Record = {}): Child { 273 | const o = (options.__typename ? options : cache["Child"] = {}) as Child; 274 | (cache.all ??= new Set()).add(o); 275 | o.__typename = "Child"; 276 | o.name = options.name ?? "name"; 277 | o.parent = maybeNew("Named", options.parent, cache, options.hasOwnProperty("parent")); 278 | return o; 279 | } 280 | 281 | factories["Child"] = newChild; 282 | 283 | export interface ParentOptions { 284 | __typename?: "Parent"; 285 | children?: Array; 286 | name?: Parent["name"]; 287 | } 288 | 289 | export function newParent(options: ParentOptions = {}, cache: Record = {}): Parent { 290 | const o = (options.__typename ? options : cache["Parent"] = {}) as Parent; 291 | (cache.all ??= new Set()).add(o); 292 | o.__typename = "Parent"; 293 | o.children = (options.children ?? []).map((i) => maybeNew("Named", i, cache, options.hasOwnProperty("children"))); 294 | o.name = options.name ?? "name"; 295 | return o; 296 | } 297 | 298 | factories["Parent"] = newParent; 299 | 300 | export interface PopularityDetailOptions { 301 | __typename?: "PopularityDetail"; 302 | code?: PopularityDetail["code"]; 303 | name?: PopularityDetail["name"]; 304 | } 305 | 306 | export function newPopularityDetail( 307 | options: PopularityDetailOptions = {}, 308 | cache: Record = {}, 309 | ): PopularityDetail { 310 | const o = (options.__typename ? options : cache["PopularityDetail"] = {}) as PopularityDetail; 311 | (cache.all ??= new Set()).add(o); 312 | o.__typename = "PopularityDetail"; 313 | o.code = options.code ?? Popularity.High; 314 | o.name = options.name ?? "High"; 315 | return o; 316 | } 317 | 318 | factories["PopularityDetail"] = newPopularityDetail; 319 | 320 | export interface SaveAuthorResultOptions { 321 | __typename?: "SaveAuthorResult"; 322 | author?: Author | AuthorOptions; 323 | } 324 | 325 | export function newSaveAuthorResult( 326 | options: SaveAuthorResultOptions = {}, 327 | cache: Record = {}, 328 | ): SaveAuthorResult { 329 | const o = (options.__typename ? options : cache["SaveAuthorResult"] = {}) as SaveAuthorResult; 330 | (cache.all ??= new Set()).add(o); 331 | o.__typename = "SaveAuthorResult"; 332 | o.author = maybeNew("Author", options.author, cache, options.hasOwnProperty("author")); 333 | return o; 334 | } 335 | 336 | factories["SaveAuthorResult"] = newSaveAuthorResult; 337 | 338 | export interface SearchResultsOptions { 339 | __typename?: "SearchResults"; 340 | result1?: SearchResult | AuthorOptions | RequireTypename | null; 341 | result2?: Author | Book | Child | Parent | PopularityDetail | Named | NamedOptions | null; 342 | result3?: Author | AuthorOptions | null; 343 | } 344 | 345 | export function newSearchResults(options: SearchResultsOptions = {}, cache: Record = {}): SearchResults { 346 | const o = (options.__typename ? options : cache["SearchResults"] = {}) as SearchResults; 347 | (cache.all ??= new Set()).add(o); 348 | o.__typename = "SearchResults"; 349 | o.result1 = maybeNewOrNull(options.result1?.__typename ?? "Author", options.result1, cache); 350 | o.result2 = maybeNewOrNull("Named", options.result2, cache); 351 | o.result3 = maybeNewOrNull("Author", options.result3, cache); 352 | return o; 353 | } 354 | 355 | factories["SearchResults"] = newSearchResults; 356 | 357 | export interface WorkingDetailOptions { 358 | __typename?: "WorkingDetail"; 359 | code?: WorkingDetail["code"]; 360 | extraField?: WorkingDetail["extraField"]; 361 | name?: WorkingDetail["name"]; 362 | } 363 | 364 | export function newWorkingDetail(options: WorkingDetailOptions = {}, cache: Record = {}): WorkingDetail { 365 | const o = (options.__typename ? options : cache["WorkingDetail"] = {}) as WorkingDetail; 366 | (cache.all ??= new Set()).add(o); 367 | o.__typename = "WorkingDetail"; 368 | o.code = options.code ?? Working.No; 369 | o.extraField = options.extraField ?? 0; 370 | o.name = options.name ?? "No"; 371 | return o; 372 | } 373 | 374 | factories["WorkingDetail"] = newWorkingDetail; 375 | 376 | export type NamedOptions = 377 | | AuthorOptions 378 | | RequireTypename 379 | | RequireTypename 380 | | RequireTypename 381 | | RequireTypename; 382 | 383 | export type NamedType = Author | Book | Child | Parent | PopularityDetail; 384 | 385 | export type NamedTypeName = "Author" | "Book" | "Child" | "Parent" | "PopularityDetail"; 386 | 387 | export function newNamed(): Author; 388 | export function newNamed(options: AuthorOptions, cache?: Record): Author; 389 | export function newNamed(options: RequireTypename, cache?: Record): Book; 390 | export function newNamed(options: RequireTypename, cache?: Record): Child; 391 | export function newNamed(options: RequireTypename, cache?: Record): Parent; 392 | export function newNamed( 393 | options: RequireTypename, 394 | cache?: Record, 395 | ): PopularityDetail; 396 | export function newNamed(options: NamedOptions = {}, cache: Record = {}): NamedType { 397 | const { __typename = "Author" } = options ?? {}; 398 | const maybeCached = Object.keys(options).length === 0 ? cache[__typename] : undefined; 399 | return maybeCached ?? maybeNew(__typename, options ?? {}, cache); 400 | } 401 | 402 | factories["Named"] = newNamed; 403 | 404 | const enumDetailNameOfPopularity = { High: "High", Low: "Low" }; 405 | 406 | function enumOrDetailOfPopularity(enumOrDetail: PopularityDetailOptions | Popularity | undefined): PopularityDetail { 407 | if (enumOrDetail === undefined) { 408 | return newPopularityDetail(); 409 | } else if (typeof enumOrDetail === "object" && "code" in enumOrDetail) { 410 | return { 411 | __typename: "PopularityDetail", 412 | code: enumOrDetail.code!, 413 | name: enumDetailNameOfPopularity[enumOrDetail.code!], 414 | ...enumOrDetail, 415 | } as PopularityDetail; 416 | } else { 417 | return newPopularityDetail({ 418 | code: enumOrDetail as Popularity, 419 | name: enumDetailNameOfPopularity[enumOrDetail as Popularity], 420 | }); 421 | } 422 | } 423 | 424 | function enumOrDetailOrNullOfPopularity( 425 | enumOrDetail: PopularityDetailOptions | Popularity | undefined | null, 426 | ): PopularityDetail | null { 427 | if (enumOrDetail === null) { 428 | return null; 429 | } 430 | return enumOrDetailOfPopularity(enumOrDetail); 431 | } 432 | 433 | const enumDetailNameOfWorking = { NO: "No", YES: "Yes" }; 434 | 435 | function enumOrDetailOfWorking(enumOrDetail: WorkingDetailOptions | Working | undefined): WorkingDetail { 436 | if (enumOrDetail === undefined) { 437 | return newWorkingDetail(); 438 | } else if (typeof enumOrDetail === "object" && "code" in enumOrDetail) { 439 | return { 440 | __typename: "WorkingDetail", 441 | code: enumOrDetail.code!, 442 | name: enumDetailNameOfWorking[enumOrDetail.code!], 443 | ...enumOrDetail, 444 | } as WorkingDetail; 445 | } else { 446 | return newWorkingDetail({ code: enumOrDetail as Working, name: enumDetailNameOfWorking[enumOrDetail as Working] }); 447 | } 448 | } 449 | 450 | function enumOrDetailOrNullOfWorking( 451 | enumOrDetail: WorkingDetailOptions | Working | undefined | null, 452 | ): WorkingDetail | null { 453 | if (enumOrDetail === null) { 454 | return null; 455 | } 456 | return enumOrDetailOfWorking(enumOrDetail); 457 | } 458 | 459 | const taggedIds: Record = { "AuthorSummary": "summary" }; 460 | let nextFactoryIds: Record = {}; 461 | 462 | export function resetFactoryIds() { 463 | nextFactoryIds = {}; 464 | } 465 | 466 | function nextFactoryId(objectName: string): string { 467 | const nextId = nextFactoryIds[objectName] || 1; 468 | nextFactoryIds[objectName] = nextId + 1; 469 | const tag = taggedIds[objectName] ?? objectName.replace(/[a-z]/g, "").toLowerCase(); 470 | return tag + ":" + nextId; 471 | } 472 | 473 | function maybeNew( 474 | type: string, 475 | value: { __typename?: string } | object | undefined, 476 | cache: Record, 477 | isSet: boolean = false, 478 | ): any { 479 | if (value === undefined) { 480 | return isSet ? undefined : cache[type] || factories[type]({}, cache); 481 | } else if ("__typename" in value && value.__typename) { 482 | return cache.all?.has(value) ? value : factories[value.__typename](value, cache); 483 | } else { 484 | return factories[type](value, cache); 485 | } 486 | } 487 | 488 | function maybeNewOrNull( 489 | type: string, 490 | value: { __typename?: string } | object | undefined | null, 491 | cache: Record, 492 | ): any { 493 | if (!value) { 494 | return null; 495 | } else if ("__typename" in value && value.__typename) { 496 | return cache.all?.has(value) ? value : factories[value.__typename](value, cache); 497 | } else { 498 | return factories[type](value, cache); 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /integration/graphql-types-only.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type InputMaybe = Maybe; 3 | export type Exact = { [K in keyof T]: T[K] }; 4 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 5 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 6 | export type MakeEmpty = { [_ in K]?: never }; 7 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 8 | /** All built-in and custom scalars, mapped to their actual values */ 9 | export type Scalars = { 10 | ID: { input: string; output: string; } 11 | String: { input: string; output: string; } 12 | Boolean: { input: boolean; output: boolean; } 13 | Int: { input: number; output: number; } 14 | Float: { input: number; output: number; } 15 | Date: { input: any; output: any; } 16 | }; 17 | 18 | export type Author = Named & { 19 | __typename?: 'Author'; 20 | anotherId: Scalars['ID']['output']; 21 | birthday?: Maybe; 22 | bookPopularities: Array; 23 | books: Array; 24 | id: Scalars['ID']['output']; 25 | name: Scalars['String']['output']; 26 | popularity: PopularityDetail; 27 | summary: AuthorSummary; 28 | working?: Maybe; 29 | workingDetail: Array; 30 | }; 31 | 32 | export type AuthorInput = { 33 | name?: InputMaybe; 34 | }; 35 | 36 | export type AuthorSummary = { 37 | __typename?: 'AuthorSummary'; 38 | amountOfSales?: Maybe; 39 | author: Author; 40 | id: Scalars['ID']['output']; 41 | numberOfBooks: Scalars['Int']['output']; 42 | }; 43 | 44 | export type Book = Named & { 45 | __typename?: 'Book'; 46 | coauthor?: Maybe; 47 | name: Scalars['String']['output']; 48 | popularity?: Maybe; 49 | reviews?: Maybe>>; 50 | status: BookStatus; 51 | }; 52 | 53 | export type BookReview = { 54 | __typename?: 'BookReview'; 55 | rating: Scalars['Int']['output']; 56 | }; 57 | 58 | export enum BookStatus { 59 | InProgress = 'IN_PROGRESS', 60 | NotStarted = 'NOT_STARTED', 61 | OnHold = 'ON_HOLD' 62 | } 63 | 64 | export type CalendarInterval = { 65 | __typename?: 'CalendarInterval'; 66 | end: Scalars['Date']['output']; 67 | start: Scalars['Date']['output']; 68 | }; 69 | 70 | export type Child = Named & { 71 | __typename?: 'Child'; 72 | name: Scalars['String']['output']; 73 | parent: Named; 74 | }; 75 | 76 | export type Mutation = { 77 | __typename?: 'Mutation'; 78 | saveAuthor: SaveAuthorResult; 79 | }; 80 | 81 | 82 | export type MutationSaveAuthorArgs = { 83 | input: AuthorInput; 84 | }; 85 | 86 | export type Named = { 87 | name: Scalars['String']['output']; 88 | }; 89 | 90 | export type Parent = Named & { 91 | __typename?: 'Parent'; 92 | children: Array; 93 | name: Scalars['String']['output']; 94 | }; 95 | 96 | export enum Popularity { 97 | High = 'High', 98 | Low = 'Low' 99 | } 100 | 101 | export type PopularityDetail = Named & { 102 | __typename?: 'PopularityDetail'; 103 | code: Popularity; 104 | name: Scalars['String']['output']; 105 | }; 106 | 107 | export type Query = { 108 | __typename?: 'Query'; 109 | authorSummaries: Array; 110 | authors: Array; 111 | search: Array; 112 | }; 113 | 114 | 115 | export type QueryAuthorsArgs = { 116 | id?: InputMaybe; 117 | }; 118 | 119 | 120 | export type QuerySearchArgs = { 121 | query: Scalars['String']['input']; 122 | }; 123 | 124 | export type SaveAuthorResult = { 125 | __typename?: 'SaveAuthorResult'; 126 | author: Author; 127 | }; 128 | 129 | export type SearchResult = Author | Book; 130 | 131 | export type SearchResults = { 132 | __typename?: 'SearchResults'; 133 | result1?: Maybe; 134 | result2?: Maybe; 135 | result3?: Maybe; 136 | }; 137 | 138 | export enum Working { 139 | No = 'NO', 140 | Yes = 'YES' 141 | } 142 | 143 | export type WorkingDetail = { 144 | __typename?: 'WorkingDetail'; 145 | code: Working; 146 | extraField: Scalars['Int']['output']; 147 | name: Scalars['String']['output']; 148 | }; 149 | -------------------------------------------------------------------------------- /integration/integration.test.ts: -------------------------------------------------------------------------------- 1 | import { jan1 } from "./testData"; 2 | 3 | import * as TypesAndFactories from "./graphql-types-and-factories"; 4 | import * as TypesOnly from "./graphql-types-only"; 5 | import * as FactoriesOnly from "./graphql-type-factories"; 6 | import * as TypesAndFactoriesWithEnumMapping from "./graphql-types-and-factories-with-enum-mapping"; 7 | 8 | type TestType = "types-imported" | "types-in-file" | "types-in-file-with-enum-mapping"; 9 | 10 | type TestObject = { 11 | newAuthor: (src?: any) => any; 12 | newAuthorSummary: (src?: any) => any; 13 | newBook: (src?: any) => any; 14 | newCalendarInterval: (src?: any) => any; 15 | newChild: (src?: any) => any; 16 | Popularity: any; 17 | resetFactoryIds: () => void; 18 | newSearchResults: (src: any) => any; 19 | }; 20 | 21 | const getTestObjects = (testType: TestType): TestObject => { 22 | if (testType === "types-imported") { 23 | return { ...TypesOnly, ...FactoriesOnly }; 24 | } else if (testType === "types-in-file") { 25 | return TypesAndFactories; 26 | } else if (testType === "types-in-file-with-enum-mapping") { 27 | return TypesAndFactoriesWithEnumMapping; 28 | } else { 29 | throw `Unsupported test type parameter provided: ${testType}`; 30 | } 31 | }; 32 | 33 | const testLabels: { [key in TestType]: string } = { 34 | "types-imported": "Types and Factories in separate files", 35 | "types-in-file": "Types and Factories in a shared file", 36 | "types-in-file-with-enum-mapping": "Types and Factories in a shared file with enum mapping", 37 | }; 38 | 39 | const getTests = (testType: TestType = "types-in-file") => { 40 | const testLabel = testLabels[testType]; 41 | 42 | const runReferenceTests = ({ 43 | newAuthor, 44 | newAuthorSummary, 45 | newBook, 46 | newCalendarInterval, 47 | newChild, 48 | Popularity, 49 | resetFactoryIds, 50 | newSearchResults, 51 | }: TestObject) => { 52 | it("does not infinite loop", () => { 53 | const a = newAuthor({}); 54 | expect(a.name).toEqual("name"); 55 | expect(a.summary.author).toStrictEqual(a); 56 | }); 57 | 58 | it("auto-factories children", () => { 59 | const a = newAuthor({ 60 | summary: { amountOfSales: 100 }, 61 | books: [{ name: "b1" }], 62 | }); 63 | expect(a.__typename).toEqual("Author"); 64 | expect(a.summary.__typename).toEqual("AuthorSummary"); 65 | expect(a.books[0].__typename).toEqual("Book"); 66 | }); 67 | 68 | it("creates unique ids", () => { 69 | resetFactoryIds(); 70 | const a1 = newAuthor({}); 71 | const a2 = newAuthor({}); 72 | expect(a1.id).toEqual("a:1"); 73 | expect(a2.id).toEqual("a:2"); 74 | // And does not assign other ID fields with author ids 75 | expect(a1.anotherId).toEqual("anotherId"); 76 | expect(a2.anotherId).toEqual("anotherId"); 77 | resetFactoryIds(); 78 | const a3 = newAuthor({}); 79 | expect(a3.id).toEqual("a:1"); 80 | }); 81 | 82 | it("creates tagged ids based on config", () => { 83 | resetFactoryIds(); 84 | const as = newAuthorSummary({}); 85 | expect(as.id).toEqual("summary:1"); 86 | }); 87 | 88 | it("fills in type name of enums", () => { 89 | const a = newAuthor({ popularity: { code: Popularity.Low } }); 90 | expect(a.popularity.__typename).toEqual("PopularityDetail"); 91 | expect(a.popularity.name).toEqual("Low"); 92 | }); 93 | 94 | it("accepts codes for enum details when nested", () => { 95 | const a = newAuthor({ books: [{ popularity: Popularity.Low }] }); 96 | expect(a.books[0].popularity?.name).toEqual("Low"); 97 | }); 98 | 99 | it("can accept types as options with nullable references", () => { 100 | const book = newBook(); 101 | const a = newAuthor({ books: [book] }); 102 | }); 103 | 104 | it("can have defaults for custom scalars", () => { 105 | const a = newCalendarInterval(); 106 | expect(a.start).toEqual(jan1); 107 | expect(a.end).toEqual(jan1); 108 | }); 109 | 110 | it("picks the 1st impl of an interface", () => { 111 | const c = newChild(); 112 | expect((c.parent as any).__typename).toEqual("Author"); 113 | }); 114 | 115 | it("keeps property as undefined if option is explicitly set to undefined", () => { 116 | const a = newAuthor({ summary: undefined }); 117 | expect(a.__typename).toEqual("Author"); 118 | expect(a.summary).toBeUndefined(); 119 | }); 120 | 121 | it("generates property if option is not passed", () => { 122 | const a = newAuthor({}); 123 | expect(a.summary.__typename).toEqual("AuthorSummary"); 124 | }); 125 | 126 | it("cascades into children that are existing entities", () => { 127 | // Given an existing factory-created instance 128 | const a = newAuthor({}); 129 | // When create a new instance 130 | const sr = newSearchResults({ 131 | result3: { 132 | // And we destructure the existing instance 133 | ...a, 134 | // And override with some partial data 135 | books: [{}], 136 | }, 137 | }); 138 | // Then the books children have typename set 139 | expect(sr.result3!.books[0].__typename).toEqual("Book"); 140 | }); 141 | }; 142 | 143 | return describe(testLabel, () => { 144 | runReferenceTests(getTestObjects(testType)); 145 | }); 146 | }; 147 | 148 | describe("typescript-factories", () => { 149 | getTests("types-in-file"); 150 | getTests("types-imported"); 151 | getTests("types-in-file-with-enum-mapping"); 152 | }); 153 | 154 | describe("enum mapping", () => { 155 | it("defaults to pascal case", () => { 156 | // Given the value of BookStatus.InProgress is pascal cased 157 | const ip = TypesAndFactories.BookStatus.InProgress; 158 | // Then our factory uses it 159 | const b = TypesAndFactories.newBook(); 160 | expect(b.status).toEqual(ip); 161 | }); 162 | 163 | it("respects enumValues=keep", () => { 164 | // Given the value of Working.NO is kept upper-cased 165 | const no = TypesAndFactoriesWithEnumMapping.Working.NO; 166 | // Then our factory uses it 167 | const wd = TypesAndFactoriesWithEnumMapping.newWorkingDetail(); 168 | expect(wd.code).toEqual(no); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /integration/schema.graphql: -------------------------------------------------------------------------------- 1 | # An entity that will be a mapped typed 2 | type Author implements Named { 3 | id: ID! 4 | # Ensure that the newAuthor factory does not call nextFactoryId for this field 5 | anotherId: ID! 6 | name: String! 7 | summary: AuthorSummary! 8 | popularity: PopularityDetail! 9 | # Example of a detail that a) has an extra field and b) is only referenced from a list 10 | workingDetail: [WorkingDetail!]! 11 | working: Working 12 | birthday: Date 13 | books: [Book!]! 14 | bookPopularities: [PopularityDetail!]! 15 | } 16 | 17 | # A DTO that is just some fields 18 | type AuthorSummary { 19 | id: ID! 20 | author: Author! 21 | numberOfBooks: Int! 22 | amountOfSales: Float 23 | } 24 | 25 | type Book implements Named { 26 | name: String! 27 | # Example of a nullable enum 28 | popularity: PopularityDetail 29 | # Example of a nullable reference 30 | coauthor: Author 31 | # Purposefully use [...]-no-bang as a boundary case 32 | reviews: [BookReview] 33 | # Example of an enum with underscores in the values 34 | status: BookStatus! 35 | } 36 | 37 | type BookReview { 38 | rating: Int! 39 | } 40 | 41 | type SearchResults { 42 | # For testing when a type name is not concrete 43 | result1: SearchResult 44 | result2: Named 45 | result3: Author 46 | } 47 | 48 | 49 | union SearchResult = Author | Book 50 | 51 | schema { 52 | query: Query 53 | mutation: Mutation 54 | } 55 | 56 | type Query { 57 | authors(id: ID): [Author!]! 58 | authorSummaries: [AuthorSummary!]! 59 | search(query: String!): [SearchResult!]! 60 | } 61 | 62 | type Mutation { 63 | saveAuthor(input: AuthorInput!): SaveAuthorResult! 64 | } 65 | 66 | type SaveAuthorResult { 67 | author: Author! 68 | } 69 | 70 | input AuthorInput { 71 | name: String 72 | } 73 | 74 | type CalendarInterval { 75 | start: Date! 76 | end: Date! 77 | } 78 | 79 | enum Popularity { 80 | Low 81 | High 82 | } 83 | 84 | type PopularityDetail implements Named { 85 | code: Popularity! 86 | name: String! 87 | } 88 | 89 | # An example of an enum type with an extra field 90 | type WorkingDetail { 91 | code: Working! 92 | name: String! 93 | extraField: Int! 94 | } 95 | 96 | enum Working { 97 | YES 98 | NO 99 | } 100 | 101 | enum BookStatus { 102 | NOT_STARTED 103 | IN_PROGRESS 104 | ON_HOLD 105 | } 106 | 107 | scalar Date 108 | 109 | interface Named { 110 | name: String! 111 | } 112 | 113 | type Parent implements Named { 114 | name: String! 115 | children: [Named!]! 116 | } 117 | 118 | type Child implements Named { 119 | name: String! 120 | # For testing newChild() picks newAuthor 121 | parent: Named! 122 | } 123 | -------------------------------------------------------------------------------- /integration/testData.ts: -------------------------------------------------------------------------------- 1 | export const jan1 = new Date(2000, 0, 1); 2 | 3 | export function newDate() { 4 | return jan1; 5 | } 6 | -------------------------------------------------------------------------------- /integration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["esnext"], 5 | "module": "commonjs", 6 | "strict": true, 7 | "esModuleInterop": false, 8 | "skipLibCheck": true, 9 | "types": ["jest"], 10 | "outDir": "build" 11 | }, 12 | "includes": ["."] 13 | } 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testMatch: ["/integration/*.test.(ts|tsx)"], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@homebound/graphql-typescript-factories", 3 | "version": "2.0.0-bump", 4 | "main": "./build/index.js", 5 | "types": "./build/", 6 | "scripts": { 7 | "build": "rm -rf build; ./node_modules/.bin/tsc", 8 | "prepack": "yarn build", 9 | "test": "./node_modules/.bin/jest --watch", 10 | "coverage": "./node_modules/.bin/jest --collectCoverage", 11 | "format": "prettier --write 'src/**/*.{ts,js,tsx,jsx}'", 12 | "graphql-codegen": "graphql-codegen --config ./integration/graphql-codegen.yml && graphql-codegen --config ./integration/graphql-codegen-types-separate.yml && graphql-codegen --config ./integration/graphql-codegen-with-enum-mapping.yml" 13 | }, 14 | "peerDependencies": { 15 | "graphql": "^15.0.0 || ^16.0.0" 16 | }, 17 | "dependencies": { 18 | "@graphql-codegen/plugin-helpers": "^5.1.0", 19 | "@graphql-codegen/visitor-plugin-common": "^5.8.0", 20 | "change-case": "^4.1.2", 21 | "ts-poet": "^6.11.0" 22 | }, 23 | "devDependencies": { 24 | "@graphql-codegen/cli": "^5.0.6", 25 | "@graphql-codegen/typescript-operations": "^4.6.1", 26 | "@types/jest": "^29.5.14", 27 | "graphql": "^16.11.0", 28 | "husky": "^3.1.0", 29 | "jest": "29.7.0", 30 | "prettier": "^3.5.3", 31 | "ts-jest": "29.3.2", 32 | "typescript": "^5.8.3" 33 | }, 34 | "husky": { 35 | "hooks": { 36 | "pre-commit": "yarn format" 37 | } 38 | }, 39 | "packageManager": "yarn@3.6.3" 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginFunction, Types } from "@graphql-codegen/plugin-helpers"; 2 | import { convertFactory, ConvertFn, NamingConvention, RawConfig } from "@graphql-codegen/visitor-plugin-common"; 3 | import { sentenceCase } from "change-case"; 4 | import { 5 | GraphQLEnumType, 6 | GraphQLField, 7 | GraphQLInterfaceType, 8 | GraphQLList, 9 | GraphQLNamedType, 10 | GraphQLNonNull, 11 | GraphQLObjectType, 12 | GraphQLOutputType, 13 | GraphQLScalarType, 14 | GraphQLSchema, 15 | GraphQLUnionType, 16 | } from "graphql"; 17 | import { Code, code, imp, Import, joinCode } from "ts-poet"; 18 | import PluginOutput = Types.PluginOutput; 19 | 20 | /** Generates `newProject({ ... })` factory functions in our `graphql-types` codegen output. */ 21 | export const plugin: PluginFunction = async (schema, documents, config: Config) => { 22 | const convert = convertFactory(config); 23 | 24 | const chunks: Code[] = []; 25 | 26 | // Create a map of interface -> implementing types 27 | const interfaceImpls: Record = {}; 28 | Object.values(schema.getTypeMap()).forEach((type) => { 29 | if (type instanceof GraphQLObjectType) { 30 | for (const i of type.getInterfaces()) { 31 | (interfaceImpls[i.name] ??= []).push(type.name); 32 | } 33 | } 34 | }); 35 | 36 | chunks.push(code`const factories: Record = {};`); 37 | chunks.push( 38 | code`type RequireTypename = Omit & Required>;`, 39 | ); 40 | 41 | const hasFactories = generateFactoryFunctions(config, convert, schema, interfaceImpls, chunks); 42 | generateInterfaceFactoryFunctions(config, interfaceImpls, chunks); 43 | generateEnumDetailHelperFunctions(config, schema, chunks); 44 | addNextIdMethods(chunks, config); 45 | generateMaybeFunctions(chunks); 46 | 47 | if (!hasFactories) { 48 | chunks.push( 49 | code`// No factories found, make sure your node_modules does not have multiple versions of the 'graphql' library`, 50 | ); 51 | } 52 | 53 | const content = await code`${chunks}`.toString(); 54 | return { content } as PluginOutput; 55 | }; 56 | 57 | function generateFactoryFunctions( 58 | config: Config, 59 | convertFn: ConvertFn, 60 | schema: GraphQLSchema, 61 | interfaceImpls: Record, 62 | chunks: Code[], 63 | ): boolean { 64 | let hasFactories = false; 65 | Object.values(schema.getTypeMap()).forEach((type) => { 66 | if (shouldCreateFactory(type)) { 67 | chunks.push(newFactory(config, convertFn, interfaceImpls, type)); 68 | hasFactories = true; 69 | } 70 | }); 71 | return hasFactories; 72 | } 73 | 74 | function generateInterfaceFactoryFunctions(config: Config, interfaceImpls: Record, chunks: Code[]) { 75 | Object.entries(interfaceImpls).forEach(([interfaceName, impls]) => { 76 | chunks.push(...newInterfaceFactory(config, interfaceName, impls)); 77 | }); 78 | } 79 | 80 | /** Makes helper methods to convert the "maybe enum / maybe enum detail" factory options into enum details. */ 81 | function generateEnumDetailHelperFunctions(config: Config, schema: GraphQLSchema, chunks: Code[]) { 82 | const usedEnumDetailTypes = new Set( 83 | Object.values(schema.getTypeMap()) 84 | .filter(shouldCreateFactory) 85 | .flatMap((type) => { 86 | return Object.values(type.getFields()) 87 | .map((f) => unwrapNullsAndLists(f.type)) 88 | .filter(isEnumDetailObject); 89 | }), 90 | ); 91 | 92 | usedEnumDetailTypes.forEach((type) => { 93 | const enumType = getRealEnumForEnumDetailObject(type); 94 | const scopedEnumTypeName = maybeImport(config, enumType.name); 95 | const scopedTypeName = maybeImport(config, type.name); 96 | 97 | const enumOrDetail = code`${type.name}Options | ${scopedEnumTypeName} | undefined`; 98 | chunks.push(code` 99 | const enumDetailNameOf${enumType.name} = { 100 | ${enumType 101 | .getValues() 102 | .map((v) => `${v.value}: "${sentenceCase(v.value)}"`) 103 | .join(", ")} 104 | }; 105 | 106 | function enumOrDetailOf${enumType.name}(enumOrDetail: ${enumOrDetail}): ${scopedTypeName} { 107 | if (enumOrDetail === undefined) { 108 | return new${type.name}(); 109 | } else if (typeof enumOrDetail === "object" && "code" in enumOrDetail) { 110 | return { 111 | __typename: "${type.name}", 112 | code: enumOrDetail.code!, 113 | name: enumDetailNameOf${enumType.name}[enumOrDetail.code!], 114 | ...enumOrDetail, 115 | } as ${scopedTypeName} 116 | } else { 117 | return new${type.name}({ 118 | code: enumOrDetail as ${scopedEnumTypeName}, 119 | name: enumDetailNameOf${enumType.name}[enumOrDetail as ${scopedEnumTypeName}], 120 | }); 121 | } 122 | } 123 | 124 | function enumOrDetailOrNullOf${enumType.name}(enumOrDetail: ${enumOrDetail} | null): ${scopedTypeName} | null { 125 | if (enumOrDetail === null) { 126 | return null; 127 | } 128 | return enumOrDetailOf${enumType.name}(enumOrDetail); 129 | } 130 | `); 131 | }); 132 | } 133 | 134 | /** Creates a `new${type}` function for the given `type`. */ 135 | function newFactory( 136 | config: Config, 137 | convertFn: ConvertFn, 138 | interfaceImpls: Record, 139 | type: GraphQLObjectType, 140 | ): Code { 141 | const typeImp = maybeImport(config, type.name); 142 | 143 | function generateListField(f: GraphQLField, fieldType: GraphQLList): string { 144 | // If this is a list of objects, initialize it as normal, but then also probe it to ensure each 145 | // passed-in value goes through `maybeNew` to ensure `__typename` is set, otherwise Apollo breaks. 146 | let elementType = fieldType.ofType as GraphQLOutputType; 147 | if (elementType instanceof GraphQLNonNull) { 148 | elementType = elementType.ofType; 149 | if (isEnumDetailObject(elementType)) { 150 | const enumType = getRealEnumForEnumDetailObject(elementType); 151 | return `o.${f.name} = (options.${f.name} ?? []).map(i => enumOrDetailOf${enumType.name}(i));`; 152 | } else if (elementType instanceof GraphQLObjectType || elementType instanceof GraphQLInterfaceType) { 153 | return `o.${f.name} = (options.${f.name} ?? []).map(i => maybeNew("${elementType.name}", i, cache, options.hasOwnProperty("${f.name}")));`; 154 | } else if (elementType instanceof GraphQLUnionType) { 155 | return `o.${f.name} = (options.${f.name} ?? []).map(i => maybeNew(i?.__typename ?? "{${elementType.getTypes()[0].name}", i, cache, options.hasOwnProperty("${f.name}")));`; 156 | } 157 | } else if (isEnumDetailObject(elementType)) { 158 | const enumType = getRealEnumForEnumDetailObject(elementType); 159 | return `o.${f.name} = (options.${f.name} ?? []).map(i => enumOrDetailOrNullOf${enumType.name}(i));`; 160 | } else if (elementType instanceof GraphQLObjectType || elementType instanceof GraphQLInterfaceType) { 161 | return `o.${f.name} = (options.${f.name} ?? []).map(i => maybeNewOrNull("${elementType.name}", i, cache));`; 162 | } else if (elementType instanceof GraphQLUnionType) { 163 | return `o.${f.name} = (options.${f.name} ?? []).map(i => maybeNewOrNull(i?.__typename ?? "{${elementType.getTypes()[0].name}", i, cache));`; 164 | } 165 | 166 | return `o.${f.name} = options.${f.name} ?? [];`; 167 | } 168 | 169 | // Instead of using `DeepPartial`, we make an explicit `AuthorOptions` for each type, primarily 170 | // b/c the `AuthorOption.books: [BookOption]` will support enum details recursively. 171 | const optionFields: Code[] = Object.values(type.getFields()).map((f) => { 172 | const fieldType = maybeDenull(f.type); 173 | const orNull = f.type instanceof GraphQLNonNull ? "" : " | null"; 174 | 175 | if (fieldType instanceof GraphQLObjectType && isEnumDetailObject(fieldType)) { 176 | return code`${f.name}?: ${fieldType.name}Options | ${getRealImportedEnum(config, fieldType)}${orNull};`; 177 | } else if (fieldType instanceof GraphQLObjectType) { 178 | return code`${f.name}?: ${fieldType.name} | ${fieldType.name}Options${orNull};`; 179 | } else if (fieldType instanceof GraphQLInterfaceType) { 180 | return code`${f.name}?: ${interfaceImpls[fieldType.name].join(" | ")} | ${maybeImport(config, fieldType.name)} | ${fieldType.name}Options${orNull};`; 181 | } else if (fieldType instanceof GraphQLUnionType) { 182 | const optionTypes = fieldType 183 | .getTypes() 184 | .map((t) => t.name) 185 | .map((name, i) => (i === 0 ? `${name}Options` : `RequireTypename<${name}Options>`)); 186 | return code`${f.name}?: ${maybeImport(config, fieldType.name)} | ${optionTypes.join(" | ")}${orNull};`; 187 | } else if (fieldType instanceof GraphQLList) { 188 | const elementType = maybeDenull(fieldType.ofType) as GraphQLNamedType; 189 | const isNonNull = fieldType.ofType instanceof GraphQLNonNull; 190 | const optionsType = code`${elementType.name}Options`; 191 | const maybeMaybeType = (type: Code) => (isNonNull ? type : code`${maybeImport(config, "Maybe")}<${type}>`); 192 | if (elementType instanceof GraphQLObjectType) { 193 | return code`${f.name}?: Array<${maybeMaybeType(code`${elementType.name} | ${optionsType}`)}>${orNull};`; 194 | } else if (elementType instanceof GraphQLInterfaceType) { 195 | return code`${f.name}?: Array<${maybeMaybeType(code`${interfaceImpls[elementType.name].join(" | ")} | ${maybeImport(config, elementType.name)} | ${optionsType}`)}>${orNull};`; 196 | } else if (elementType instanceof GraphQLUnionType) { 197 | const optionTypes = elementType 198 | .getTypes() 199 | .map((t) => t.name) 200 | .map((name, i) => (i === 0 ? `${name}Options` : `RequireTypename<${name}Options>`)); 201 | return code`${f.name}?: Array<${maybeMaybeType(code`${maybeImport(config, elementType.name)} | ${optionTypes.join(" | ")}`)}>${orNull};`; 202 | } else { 203 | return code`${f.name}?: ${typeImp}["${f.name}"]${orNull};`; 204 | } 205 | } else { 206 | return code`${f.name}?: ${typeImp}["${f.name}"];`; 207 | } 208 | }); 209 | 210 | const factory = code` 211 | export interface ${type.name}Options { 212 | __typename?: '${type.name}'; 213 | ${joinCode(optionFields, { on: "\n" })} 214 | } 215 | 216 | export function new${type.name}(options: ${type.name}Options = {}, cache: Record = {}): ${typeImp} { 217 | const o = (options.__typename ? options : cache["${type.name}"] = {}) as ${typeImp}; 218 | (cache.all ??= new Set()).add(o); 219 | o.__typename = '${type.name}'; 220 | ${Object.values(type.getFields()).map((f) => { 221 | if (f.type instanceof GraphQLNonNull) { 222 | const fieldType = f.type.ofType; 223 | if (isEnumDetailObject(fieldType)) { 224 | const enumType = getRealEnumForEnumDetailObject(fieldType); 225 | return `o.${f.name} = enumOrDetailOf${enumType.name}(options.${f.name});`; 226 | } else if (fieldType instanceof GraphQLList) { 227 | return generateListField(f, fieldType); 228 | } else if (fieldType instanceof GraphQLObjectType || fieldType instanceof GraphQLInterfaceType) { 229 | return `o.${f.name} = maybeNew("${fieldType.name}", options.${f.name}, cache, options.hasOwnProperty("${f.name}"));`; 230 | } else if (fieldType instanceof GraphQLUnionType) { 231 | return `o.${f.name} = maybeNew(options.${f.name}?.__typename ?? "${fieldType.getTypes()[0].name}", options.${f.name}, cache);`; 232 | } else { 233 | return code`o.${f.name} = options.${f.name} ?? ${getInitializer(config, convertFn, type, f, fieldType)};`; 234 | } 235 | } else if (isEnumDetailObject(f.type)) { 236 | const enumType = getRealEnumForEnumDetailObject(f.type); 237 | return `o.${f.name} = enumOrDetailOrNullOf${enumType.name}(options.${f.name});`; 238 | } else if (f.type instanceof GraphQLList) { 239 | return generateListField(f, f.type); 240 | } else if (f.type instanceof GraphQLObjectType || f.type instanceof GraphQLInterfaceType) { 241 | return `o.${f.name} = maybeNewOrNull("${(f.type as any).name}", options.${f.name}, cache);`; 242 | } else if (f.type instanceof GraphQLUnionType) { 243 | return `o.${f.name} = maybeNewOrNull(options.${f.name}?.__typename ?? "${f.type.getTypes()[0].name}", options.${f.name}, cache);`; 244 | } else { 245 | return `o.${f.name} = options.${f.name} ?? null;`; 246 | } 247 | })} 248 | return o; 249 | } 250 | 251 | factories["${type.name}"] = new${type.name}; 252 | `; 253 | 254 | return factory; 255 | } 256 | 257 | function generateMaybeFunctions(chunks: Code[]): void { 258 | const maybeFunctions = code` 259 | function maybeNew(type: string, value: { __typename?: string } | object | undefined, cache: Record, isSet: boolean = false): any { 260 | if (value === undefined) { 261 | return isSet ? undefined : cache[type] || factories[type]({}, cache) 262 | } else if ("__typename" in value && value.__typename) { 263 | return cache.all?.has(value) ? value : factories[value.__typename](value, cache); 264 | } else { 265 | return factories[type](value, cache); 266 | } 267 | } 268 | 269 | function maybeNewOrNull(type: string, value: { __typename?: string } | object | undefined | null, cache: Record): any { 270 | if (!value) { 271 | return null; 272 | } else if ("__typename" in value && value.__typename) { 273 | return cache.all?.has(value) ? value : factories[value.__typename](value, cache); 274 | } else { 275 | return factories[type](value, cache); 276 | } 277 | }`; 278 | chunks.push(maybeFunctions); 279 | } 280 | 281 | /** Creates a `new${type}` function for the given `type`. */ 282 | function newInterfaceFactory(config: Config, interfaceName: string, impls: string[]): Code[] { 283 | const defaultImpl = impls[0] || fail(`Interface ${interfaceName} is unused`); 284 | 285 | return [ 286 | code` 287 | export type ${interfaceName}Options = ${impls.map((name, i) => (i === 0 ? `${name}Options` : `RequireTypename<${name}Options>`)).join(" | ")}; 288 | `, 289 | 290 | code` 291 | export type ${interfaceName}Type = ${joinCode( 292 | impls.map((type) => maybeImport(config, type)), 293 | { on: " | " }, 294 | )}; 295 | `, 296 | 297 | code` 298 | export type ${interfaceName}TypeName = ${impls.map((n) => `"${n}"`).join(" | ")}; 299 | `, 300 | 301 | code` 302 | export function new${interfaceName}(): ${defaultImpl}; 303 | ${impls.map((name, i) => code`export function new${interfaceName}(options: ${i === 0 ? `${name}Options` : `RequireTypename<${name}Options>`}, cache?: Record): ${name};`)} 304 | export function new${interfaceName}(options: ${interfaceName}Options = {}, cache: Record = {}): ${interfaceName}Type { 305 | const { __typename = "${defaultImpl}" } = options ?? {}; 306 | const maybeCached = Object.keys(options).length === 0 ? cache[__typename] : undefined 307 | return maybeCached ?? maybeNew(__typename, options ?? {}, cache); 308 | } 309 | `, 310 | 311 | code` 312 | factories["${interfaceName}"] = new${interfaceName}; 313 | `, 314 | ]; 315 | } 316 | 317 | /** Returns a default value for the given field's type, i.e. strings are "", ints are 0, arrays are []. */ 318 | function getInitializer( 319 | config: Config, 320 | convertFn: ConvertFn, 321 | object: GraphQLObjectType, 322 | field: GraphQLField, 323 | type: GraphQLOutputType, 324 | ): string | Code { 325 | if (type instanceof GraphQLList) { 326 | // We could potentially make a dummy entry in every list, but would we risk infinite loops between parents/children? 327 | return `[]`; 328 | } else if (type instanceof GraphQLEnumType) { 329 | const defaultEnumValue = type.getValues()[0]; 330 | // The default behavior of graphql-codegen is that enums do drop underscores, but 331 | // type names don't; emulate that by passing `transformUnderscore` here. If the user 332 | // _does_ have it overridden in their config, then that causes enums & types to be 333 | // treated exactly the same. 334 | // 335 | // Todo: we should also check `ignoreEnumValuesFromSchema` and `enumValues` in the config: 336 | // https://github.com/dotansimha/graphql-code-generator/blob/master/packages/plugins/other/visitor-plugin-common/src/base-types-visitor.ts#L921 337 | const name = convertFn(defaultEnumValue.astNode || defaultEnumValue.value, { transformUnderscore: true }); 338 | return code`${maybeImport(config, type.name)}.${name}`; 339 | } else if (type instanceof GraphQLScalarType) { 340 | if (type.name === "Int") { 341 | return `0`; 342 | } else if (type.name === "Boolean") { 343 | return `false`; 344 | } else if (field.name === "id" && type.name === "ID") { 345 | // Only call for the `id` field, since we don't want to generate new IDs if the object contains other ID fields 346 | return `nextFactoryId("${object.name}")`; 347 | } else if (type.name === "String" || type.name === "ID") { 348 | // Treat String and ID fields the same, as long as it is not the `id: ID` field 349 | const maybeCode = isEnumDetailObject(object) && object.getFields()["code"]; 350 | if (maybeCode) { 351 | const value = getRealEnumForEnumDetailObject(object).getValues()[0].value; 352 | return `"${sentenceCase(value)}"`; 353 | } else { 354 | return `"${field.name}"`; 355 | } 356 | } 357 | const defaultFromConfig = config.scalarDefaults?.[type.name]; 358 | if (defaultFromConfig) { 359 | return code`${toImp(defaultFromConfig)}()`; 360 | } 361 | return `"" as any`; 362 | } 363 | return `undefined as any`; 364 | } 365 | 366 | /** Look for the FooDetail/code/name pattern of our enum detail objects. */ 367 | function isEnumDetailObject(object: GraphQLOutputType): object is GraphQLObjectType { 368 | return ( 369 | object instanceof GraphQLObjectType && 370 | object.name.endsWith("Detail") && 371 | Object.keys(object.getFields()).length >= 2 && 372 | !!object.getFields()["code"] && 373 | !!object.getFields()["name"] 374 | ); 375 | } 376 | 377 | function getRealEnumForEnumDetailObject(detailObject: GraphQLOutputType): GraphQLEnumType { 378 | return unwrapNotNull((unwrapNotNull(detailObject) as GraphQLObjectType).getFields()["code"]!.type) as GraphQLEnumType; 379 | } 380 | 381 | function getRealImportedEnum(config: Config, detailObject: GraphQLOutputType): Code { 382 | return maybeImport(config, getRealEnumForEnumDetailObject(detailObject).name); 383 | } 384 | 385 | function unwrapNotNull(type: GraphQLOutputType): GraphQLOutputType { 386 | if (type instanceof GraphQLNonNull) { 387 | return type.ofType; 388 | } else { 389 | return type; 390 | } 391 | } 392 | 393 | /** Unwrap `Foo!` -> `Foo` and `[Foo!]!` -> `Foo`. */ 394 | function unwrapNullsAndLists(type: GraphQLOutputType): GraphQLOutputType { 395 | if (type instanceof GraphQLNonNull) { 396 | type = type.ofType; 397 | } 398 | if (type instanceof GraphQLList) { 399 | type = type.ofType; 400 | } 401 | if (type instanceof GraphQLNonNull) { 402 | type = type.ofType; 403 | } 404 | return type; 405 | } 406 | 407 | function shouldCreateFactory(type: GraphQLNamedType): type is GraphQLObjectType { 408 | return ( 409 | type instanceof GraphQLObjectType && 410 | !type.name.startsWith("__") && 411 | type.name !== "Mutation" && 412 | type.name !== "Query" 413 | ); 414 | } 415 | 416 | function addNextIdMethods(chunks: Code[], config: Config): void { 417 | chunks.push(code` 418 | const taggedIds: Record = ${config.taggedIds || "{}"}; 419 | let nextFactoryIds: Record = {}; 420 | 421 | export function resetFactoryIds() { 422 | nextFactoryIds = {}; 423 | } 424 | 425 | function nextFactoryId(objectName: string): string { 426 | const nextId = nextFactoryIds[objectName] || 1; 427 | nextFactoryIds[objectName] = nextId + 1; 428 | const tag = taggedIds[objectName] ?? objectName.replace(/[a-z]/g, "").toLowerCase(); 429 | return tag + ":" + nextId; 430 | } 431 | `); 432 | } 433 | 434 | function maybeImport(config: Config, typeName: string): Code { 435 | return code`${!!config.typesFilePath ? imp(`${typeName}@${config.typesFilePath}`) : typeName}`; 436 | } 437 | 438 | function maybeDenull(o: GraphQLOutputType): GraphQLOutputType { 439 | return o instanceof GraphQLNonNull ? o.ofType : o; 440 | } 441 | 442 | /** The config values we read from the graphql-codegen.yml file. */ 443 | export type Config = RawConfig & { 444 | scalarDefaults?: Record; 445 | taggedIds?: Record; 446 | typesFilePath?: string; 447 | namingConvention?: NamingConvention; 448 | }; 449 | 450 | // Maps the graphql-code-generation convention of `@src/context#Context` to ts-poet's `Context@@src/context`. 451 | export function toImp(spec: string): Import { 452 | const [path, symbol] = spec.split("#"); 453 | return imp(`${symbol}@${path}`); 454 | } 455 | 456 | export function fail(message?: string): never { 457 | throw new Error(message || "Failed"); 458 | } 459 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["esnext"], 5 | "module": "commonjs", 6 | "strict": true, 7 | "esModuleInterop": false, 8 | "skipLibCheck": true, 9 | "inlineSourceMap": true, 10 | "types": ["jest"], 11 | "outDir": "build" 12 | }, 13 | "includes": ["src"], 14 | "exclude": ["integration"] 15 | } 16 | --------------------------------------------------------------------------------