diff --git a/.azure-devops/graphitation-release.yml b/.azure-devops/graphitation-release.yml index e5ff2a284..6d400ddce 100644 --- a/.azure-devops/graphitation-release.yml +++ b/.azure-devops/graphitation-release.yml @@ -2,6 +2,7 @@ pr: none trigger: - main - alloy/relay-apollo-duct-tape + - jvejr/context-and-async-hooks-alpha variables: - group: InfoSec-SecurityResults diff --git a/.vscode/settings.json b/.vscode/settings.json index 8892811ad..3b750d0cc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, - "cSpell.words": ["Daichi", "fukuda", "Kadji"] + "cSpell.words": ["Daichi", "fukuda", "Kadji"], + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/examples/apollo-watch-fragments/package.json b/examples/apollo-watch-fragments/package.json index c7aca3b1f..b9e68b2c6 100644 --- a/examples/apollo-watch-fragments/package.json +++ b/examples/apollo-watch-fragments/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@apollo/client": "~3.6.0", - "@graphitation/apollo-react-relay-duct-tape": "^1.3.5", + "@graphitation/apollo-react-relay-duct-tape": "^1.3.6", "@graphitation/graphql-js-tag": "^0.9.4", "@graphql-tools/schema": "^9.0.19", "graphql": "^15.6.0", @@ -36,9 +36,9 @@ ] }, "devDependencies": { - "@graphitation/apollo-react-relay-duct-tape-compiler": "^1.6.4", + "@graphitation/apollo-react-relay-duct-tape-compiler": "^1.6.5", "@graphitation/embedded-document-artefact-loader": "^0.8.5", - "@graphitation/supermassive": "^3.6.2", + "@graphitation/supermassive": "^3.6.3", "@graphql-codegen/cli": "2.2.0", "@graphql-codegen/typescript": "2.2.2", "@graphql-codegen/typescript-resolvers": "^2.2.1", diff --git a/examples/supermassive-todomvc/package.json b/examples/supermassive-todomvc/package.json index 9d40f595a..2e001f20b 100644 --- a/examples/supermassive-todomvc/package.json +++ b/examples/supermassive-todomvc/package.json @@ -15,10 +15,10 @@ "dependencies": { "@apollo/client": "~3.6.0", "@babel/runtime": "^7.12.0", - "@graphitation/apollo-react-relay-duct-tape": "^1.3.5", - "@graphitation/apollo-react-relay-duct-tape-compiler": "^1.6.4", + "@graphitation/apollo-react-relay-duct-tape": "^1.3.6", + "@graphitation/apollo-react-relay-duct-tape-compiler": "^1.6.5", "@graphitation/graphql-js-tag": "^0.9.4", - "@graphitation/supermassive": "^3.6.2", + "@graphitation/supermassive": "^3.6.3", "@graphitation/ts-transform-graphql-js-tag": "^1.4.4", "concurrently": "^6.5.1", "graphql": "^15.6.1", @@ -58,7 +58,7 @@ "@types/uuid": "^8.3.1", "graphql-let": "^0.18.5", "prettier": "^2.8.7", - "@graphitation/webpack-loader": "^1.0.12", + "@graphitation/webpack-loader": "^1.0.13", "typescript": "^5.5.3", "webpack-dev-server": "^4.13.3" } diff --git a/package.json b/package.json index 6d0c7990b..ddb51adef 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,10 @@ "lint": "lage lint --continue", "lage": "lage", "ci": "yarn lage build types test lint && yarn checkchange", - "beachball": "beachball -b origin/main", + "beachball": "beachball -b origin/jvejr/context-and-async-hooks-alpha", "change": "yarn beachball change", "checkchange": "yarn beachball check", - "release": "yarn beachball publish -t latest", + "release": "yarn beachball publish -t alpha", "postinstall": "patch-package" }, "devDependencies": { diff --git a/packages/apollo-react-relay-duct-tape-compiler/CHANGELOG.json b/packages/apollo-react-relay-duct-tape-compiler/CHANGELOG.json index 408f14a17..93122eb6b 100644 --- a/packages/apollo-react-relay-duct-tape-compiler/CHANGELOG.json +++ b/packages/apollo-react-relay-duct-tape-compiler/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@graphitation/apollo-react-relay-duct-tape-compiler", "entries": [ + { + "date": "Wed, 30 Oct 2024 09:41:18 GMT", + "version": "1.6.5", + "tag": "@graphitation/apollo-react-relay-duct-tape-compiler_v1.6.5", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@graphitation/apollo-react-relay-duct-tape-compiler", + "comment": "Bump @graphitation/supermassive to v3.6.3", + "commit": "not available" + } + ] + } + }, { "date": "Thu, 24 Oct 2024 13:15:11 GMT", "version": "1.6.4", diff --git a/packages/apollo-react-relay-duct-tape-compiler/CHANGELOG.md b/packages/apollo-react-relay-duct-tape-compiler/CHANGELOG.md index 03e2b6c1d..685b0c340 100644 --- a/packages/apollo-react-relay-duct-tape-compiler/CHANGELOG.md +++ b/packages/apollo-react-relay-duct-tape-compiler/CHANGELOG.md @@ -1,9 +1,17 @@ # Change Log - @graphitation/apollo-react-relay-duct-tape-compiler - + +## 1.6.5 + +Wed, 30 Oct 2024 09:41:18 GMT + +### Patches + +- Bump @graphitation/supermassive to v3.6.3 + ## 1.6.4 Thu, 24 Oct 2024 13:15:11 GMT diff --git a/packages/apollo-react-relay-duct-tape-compiler/package.json b/packages/apollo-react-relay-duct-tape-compiler/package.json index d18297958..9296ea83d 100644 --- a/packages/apollo-react-relay-duct-tape-compiler/package.json +++ b/packages/apollo-react-relay-duct-tape-compiler/package.json @@ -2,7 +2,7 @@ "name": "@graphitation/apollo-react-relay-duct-tape-compiler", "description": "The build tools to cater to @graphitation/apollo-react-relay-duct-tape's needs.", "license": "MIT", - "version": "1.6.4", + "version": "1.6.5", "main": "./src/index.ts", "bin": { "duct-tape-compiler": "./src/cli-cjs.js" @@ -42,7 +42,7 @@ }, "peerDependencies": { "graphql": "^15.0.0", - "@graphitation/supermassive": "^3.6.2", + "@graphitation/supermassive": "^3.6.3", "typescript": "^5.5.3" }, "publishConfig": { diff --git a/packages/apollo-react-relay-duct-tape/CHANGELOG.json b/packages/apollo-react-relay-duct-tape/CHANGELOG.json index f970e33a0..1f4360330 100644 --- a/packages/apollo-react-relay-duct-tape/CHANGELOG.json +++ b/packages/apollo-react-relay-duct-tape/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@graphitation/apollo-react-relay-duct-tape", "entries": [ + { + "date": "Wed, 30 Oct 2024 09:41:18 GMT", + "version": "1.3.6", + "tag": "@graphitation/apollo-react-relay-duct-tape_v1.3.6", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@graphitation/apollo-react-relay-duct-tape", + "comment": "Bump @graphitation/apollo-react-relay-duct-tape-compiler to v1.6.5", + "commit": "not available" + } + ] + } + }, { "date": "Thu, 24 Oct 2024 13:15:11 GMT", "version": "1.3.5", diff --git a/packages/apollo-react-relay-duct-tape/CHANGELOG.md b/packages/apollo-react-relay-duct-tape/CHANGELOG.md index 8f499e2ed..365ca87c4 100644 --- a/packages/apollo-react-relay-duct-tape/CHANGELOG.md +++ b/packages/apollo-react-relay-duct-tape/CHANGELOG.md @@ -1,9 +1,17 @@ # Change Log - @graphitation/apollo-react-relay-duct-tape - + +## 1.3.6 + +Wed, 30 Oct 2024 09:41:18 GMT + +### Patches + +- Bump @graphitation/apollo-react-relay-duct-tape-compiler to v1.6.5 + ## 1.3.5 Thu, 24 Oct 2024 13:15:11 GMT diff --git a/packages/apollo-react-relay-duct-tape/package.json b/packages/apollo-react-relay-duct-tape/package.json index 56ef34b6b..281f979ab 100644 --- a/packages/apollo-react-relay-duct-tape/package.json +++ b/packages/apollo-react-relay-duct-tape/package.json @@ -2,7 +2,7 @@ "name": "@graphitation/apollo-react-relay-duct-tape", "description": "A compatibility wrapper that provides the react-relay API on top of Apollo Client.", "license": "MIT", - "version": "1.3.5", + "version": "1.3.6", "repository": { "type": "git", "url": "https://github.com/microsoft/graphitation.git", @@ -30,7 +30,7 @@ "graphql": "^15.0.0", "monorepo-scripts": "*", "react": "^18.2.0", - "@graphitation/apollo-react-relay-duct-tape-compiler": "^1.6.4", + "@graphitation/apollo-react-relay-duct-tape-compiler": "^1.6.5", "ts-expect": "^1.3.0" }, "peerDependencies": { diff --git a/packages/cli/CHANGELOG.json b/packages/cli/CHANGELOG.json index 8a1ff4674..a90fcc794 100644 --- a/packages/cli/CHANGELOG.json +++ b/packages/cli/CHANGELOG.json @@ -1,6 +1,137 @@ { "name": "@graphitation/cli", "entries": [ + { + "date": "Thu, 14 Nov 2024 14:18:34 GMT", + "version": "1.13.0-alpha.4", + "tag": "@graphitation/cli_v1.13.0-alpha.4", + "comments": { + "none": [ + { + "author": "beachball", + "package": "@graphitation/cli", + "comment": "Bump @graphitation/ts-codegen to v2.13.0-alpha.6", + "commit": "not available" + } + ] + } + }, + { + "date": "Wed, 13 Nov 2024 20:52:03 GMT", + "version": "1.13.0-alpha.4", + "tag": "@graphitation/cli_v1.13.0-alpha.4", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/cli", + "commit": "e9a34254f389d06042d7deb11bca819dd2d642ed", + "comment": "Solution cleanup - new cli parameters and inheritance fixes" + }, + { + "author": "beachball", + "package": "@graphitation/cli", + "comment": "Bump @graphitation/ts-codegen to v2.13.0-alpha.5", + "commit": "not available" + } + ] + } + }, + { + "date": "Mon, 11 Nov 2024 14:13:12 GMT", + "version": "1.13.0-alpha.3", + "tag": "@graphitation/cli_v1.13.0-alpha.3", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/cli", + "commit": "f3b1a34db0e9fc7bbf4ddf972fd509f4e7d9f5ac", + "comment": "Added import state machines templating" + }, + { + "author": "beachball", + "package": "@graphitation/cli", + "comment": "Bump @graphitation/ts-codegen to v2.13.0-alpha.4", + "commit": "not available" + } + ] + } + }, + { + "date": "Mon, 04 Nov 2024 17:34:20 GMT", + "version": "1.13.0-alpha.2", + "tag": "@graphitation/cli_v1.13.0-alpha.2", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/cli", + "commit": "dd33b80a0c6975c118d8376cbba61cda706802c7", + "comment": "Added context namespace into the codegen" + }, + { + "author": "beachball", + "package": "@graphitation/cli", + "comment": "Bump @graphitation/ts-codegen to v2.13.0-alpha.3", + "commit": "not available" + } + ] + } + }, + { + "date": "Wed, 30 Oct 2024 20:39:21 GMT", + "version": "1.13.0-alpha.1", + "tag": "@graphitation/cli_v1.13.0-alpha.1", + "comments": { + "none": [ + { + "author": "beachball", + "package": "@graphitation/cli", + "comment": "Bump @graphitation/ts-codegen to v2.13.0-alpha.2", + "commit": "not available" + } + ] + } + }, + { + "date": "Wed, 30 Oct 2024 10:07:52 GMT", + "version": "1.13.0-alpha.1", + "tag": "@graphitation/cli_v1.13.0-alpha.1", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/cli", + "commit": "22df54e57ce24fd771c02b1e52dc3bd8feb73187", + "comment": "CLI alpha bump" + } + ] + } + }, + { + "date": "Wed, 30 Oct 2024 09:41:18 GMT", + "version": "1.13.0", + "tag": "@graphitation/cli_v1.13.0", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/cli", + "commit": "d69740884c12998eb1fb43ce8dc46afc1a577956", + "comment": "metadata written to a file and tests fixed" + } + ], + "patch": [ + { + "author": "beachball", + "package": "@graphitation/cli", + "comment": "Bump @graphitation/ts-codegen to v2.13.0-alpha.1", + "commit": "not available" + } + ] + } + }, { "date": "Thu, 24 Oct 2024 13:15:11 GMT", "version": "1.12.1", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index d81fe5bfb..ab5afa0ef 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,9 +1,56 @@ # Change Log - @graphitation/cli - + +## 1.13.0-alpha.4 + +Wed, 13 Nov 2024 20:52:03 GMT + +### Changes + +- Solution cleanup - new cli parameters and inheritance fixes (77059398+vejrj@users.noreply.github.com) +- Bump @graphitation/ts-codegen to v2.13.0-alpha.5 + +## 1.13.0-alpha.3 + +Mon, 11 Nov 2024 14:13:12 GMT + +### Changes + +- Added import state machines templating (77059398+vejrj@users.noreply.github.com) +- Bump @graphitation/ts-codegen to v2.13.0-alpha.4 + +## 1.13.0-alpha.2 + +Mon, 04 Nov 2024 17:34:20 GMT + +### Changes + +- Added context namespace into the codegen (77059398+vejrj@users.noreply.github.com) +- Bump @graphitation/ts-codegen to v2.13.0-alpha.3 + +## 1.13.0-alpha.1 + +Wed, 30 Oct 2024 10:07:52 GMT + +### Changes + +- CLI alpha bump (77059398+vejrj@users.noreply.github.com) + +## 1.13.0 + +Wed, 30 Oct 2024 09:41:18 GMT + +### Patches + +- Bump @graphitation/ts-codegen to v2.13.0-alpha.1 + +### Changes + +- metadata written to a file and tests fixed (77059398+vejrj@users.noreply.github.com) + ## 1.12.1 Thu, 24 Oct 2024 13:15:11 GMT diff --git a/packages/cli/package.json b/packages/cli/package.json index 68014b5ab..1467d2bbd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@graphitation/cli", "license": "MIT", - "version": "1.12.1", + "version": "1.13.0-alpha.4", "bin": { "supermassive": "./bin/supermassive.js" }, @@ -24,7 +24,7 @@ }, "dependencies": { "@graphitation/supermassive-extractors": "^2.2.5", - "@graphitation/ts-codegen": "^2.12.1", + "@graphitation/ts-codegen": "^2.13.0-alpha.6", "commander": "^8.3.0", "fast-glob": "^3.2.12", "graphql": "^15.6.1" diff --git a/packages/cli/src/supermassive.ts b/packages/cli/src/supermassive.ts index 2276cb0fe..d4b2a16a9 100644 --- a/packages/cli/src/supermassive.ts +++ b/packages/cli/src/supermassive.ts @@ -10,8 +10,8 @@ import * as glob from "fast-glob"; type GenerateInterfacesOptions = { outputDir?: string; - contextImport?: string; - contextName?: string; + contextTypePath?: string; + contextTypeName?: string; enumsImport?: string; legacy?: boolean; legacyModels?: boolean; @@ -19,6 +19,10 @@ type GenerateInterfacesOptions = { enumMigrationJsonFile?: string; enumMigrationExceptionsJsonFile?: string; generateOnlyEnums?: boolean; + contextSubTypeNameTemplate?: string; + contextSubTypePathTemplate?: string; + defaultContextSubTypePath?: string; + defaultContextSubTypeName?: string; scope?: string; }; @@ -45,10 +49,26 @@ export function supermassive(): Command { "output directory relative to file, default generated", ) .option( - "-ci, --context-import [contextImport]", + "-ci, --context-type-path [contextTypePath]", "from where to import context", ) - .option("-cn, --context-name [contextName]", "Context name") + .option("-cn, --context-type-name [contextTypeName]", "Context type name") + .option( + "-dcp, --default-context-sub-type-path [defaultContextSubTypePath]", + "from where to import context", + ) + .option( + "-dcn, --default-context-sub-type-name [defaultContextSubTypeName]", + "Context name", + ) + .option( + "-cm, --context-sub-type-name-template [contextSubTypeNameTemplate]", + "context namespace name", + ) + .option( + "-cm, --context-sub-type-path-template [contextSubTypePathTemplate]", + "context namespace path", + ) .option("-ei, --enums-import [enumsImport]", "from where to import enums") .option("-l, --legacy", "generate legacy types") .option("--legacy-models", "do not use models for object types") @@ -58,14 +78,6 @@ export function supermassive(): Command { ) .option("--generate-only-enums", "Generate only enum file") .option("--scope [scope]", "generate models only for scope") - .option( - "--enum-migration-json-file [enumMigrationJsonFile]", - "File containing array of enum names, which should be migrated to string unions", - ) - .option( - "--enum-migration-exceptions-json-file [enumMigrationExceptionsJsonFile]", - "File containing array of enum names, which should remain typescript enums", - ) .description("generate interfaces and models") .action( async (inputs: Array, options: GenerateInterfacesOptions) => { @@ -91,16 +103,19 @@ function getFiles(inputs: Array) { .flat() .filter(Boolean); } -function getContextPath(outputDir: string, contextImport: string | undefined) { - if (!contextImport) { +function getContextPath( + outputDir: string, + contextTypePath: string | undefined, +) { + if (!contextTypePath) { return; } - if (!contextImport.startsWith(".")) { - return contextImport; + if (!contextTypePath.startsWith(".")) { + return contextTypePath; } - const contextDir = path.join(process.cwd(), contextImport); + const contextDir = path.join(process.cwd(), contextTypePath); return path .relative(outputDir, contextDir) @@ -130,56 +145,25 @@ async function generateInterfaces( path.dirname(fullPath), options.outputDir ? options.outputDir : "__generated__", ); - let enumNamesToMigrate; - let enumNamesToKeep; - if (options.enumMigrationJsonFile) { - const content = JSON.parse( - await fs.readFile( - path.join(process.cwd(), options.enumMigrationJsonFile), - { - encoding: "utf-8", - }, - ), - ); - - if (!Array.isArray(content)) { - throw new Error("enumMigrationJsonFile doesn't contain an array"); - } - - enumNamesToMigrate = content; - } - - if (options.enumMigrationExceptionsJsonFile) { - const content = JSON.parse( - await fs.readFile( - path.join(process.cwd(), options.enumMigrationExceptionsJsonFile), - { - encoding: "utf-8", - }, - ), - ); - - if (!Array.isArray(content)) { - throw new Error( - "enumMigrationExceptionsJsonFile doesn't contain an array", - ); - } - - enumNamesToKeep = content; - } const result = generateTS(document, { outputPath, documentPath: fullPath, - contextImport: getContextPath(outputPath, options.contextImport) || null, - contextName: options.contextName, + contextTypePath: + getContextPath(outputPath, options.contextTypePath) || null, + contextTypeName: options.contextTypeName, + contextSubTypeNameTemplate: options.contextSubTypeNameTemplate, + contextSubTypePathTemplate: options.contextSubTypePathTemplate, + defaultContextSubTypePath: getContextPath( + outputPath, + options.defaultContextSubTypePath, + ), + defaultContextSubTypeName: options.defaultContextSubTypeName, enumsImport: getContextPath(outputPath, options.enumsImport) || null, legacyCompat: !!options.legacy, legacyNoModelsForObjects: !!options.legacyModels, useStringUnionsInsteadOfEnums: !!options.useStringUnionsInsteadOfEnums, generateOnlyEnums: !!options.generateOnlyEnums, - enumNamesToMigrate, - enumNamesToKeep, modelScope: options.scope || null, }); @@ -195,6 +179,16 @@ async function generateInterfaces( ), ); + if (result.contextMappingOutput) { + outputs.push( + fs.writeFile( + path.join(outputPath, "schema-context-mapping-metadata.json"), + JSON.stringify(result.contextMappingOutput, null, 2), + { encoding: "utf-8" }, + ), + ); + } + await Promise.all(outputs); } } diff --git a/packages/graphql-codegen-supermassive-schema-extraction-plugin/CHANGELOG.json b/packages/graphql-codegen-supermassive-schema-extraction-plugin/CHANGELOG.json index 5a0ed92e2..ae42bf9d1 100644 --- a/packages/graphql-codegen-supermassive-schema-extraction-plugin/CHANGELOG.json +++ b/packages/graphql-codegen-supermassive-schema-extraction-plugin/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@graphitation/graphql-codegen-supermassive-schema-extraction-plugin", "entries": [ + { + "date": "Wed, 30 Oct 2024 09:41:18 GMT", + "version": "2.0.15", + "tag": "@graphitation/graphql-codegen-supermassive-schema-extraction-plugin_v2.0.15", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@graphitation/graphql-codegen-supermassive-schema-extraction-plugin", + "comment": "Bump @graphitation/supermassive to v3.6.3", + "commit": "not available" + } + ] + } + }, { "date": "Thu, 24 Oct 2024 13:15:11 GMT", "version": "2.0.14", diff --git a/packages/graphql-codegen-supermassive-schema-extraction-plugin/CHANGELOG.md b/packages/graphql-codegen-supermassive-schema-extraction-plugin/CHANGELOG.md index de03a506f..5e59822d9 100644 --- a/packages/graphql-codegen-supermassive-schema-extraction-plugin/CHANGELOG.md +++ b/packages/graphql-codegen-supermassive-schema-extraction-plugin/CHANGELOG.md @@ -1,9 +1,17 @@ # Change Log - @graphitation/graphql-codegen-supermassive-schema-extraction-plugin - + +## 2.0.15 + +Wed, 30 Oct 2024 09:41:18 GMT + +### Patches + +- Bump @graphitation/supermassive to v3.6.3 + ## 2.0.14 Thu, 24 Oct 2024 13:15:11 GMT diff --git a/packages/graphql-codegen-supermassive-schema-extraction-plugin/package.json b/packages/graphql-codegen-supermassive-schema-extraction-plugin/package.json index 7ba0f5082..793336342 100644 --- a/packages/graphql-codegen-supermassive-schema-extraction-plugin/package.json +++ b/packages/graphql-codegen-supermassive-schema-extraction-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@graphitation/graphql-codegen-supermassive-schema-extraction-plugin", "license": "MIT", - "version": "2.0.14", + "version": "2.0.15", "main": "./src/index.ts", "repository": { "type": "git", @@ -23,7 +23,7 @@ "@graphql-codegen/plugin-helpers": ">= 1.18.0 < 2" }, "dependencies": { - "@graphitation/supermassive": "^3.6.2", + "@graphitation/supermassive": "^3.6.3", "graphql": "^15.0.0" }, "sideEffects": false, diff --git a/packages/graphql-codegen-supermassive-typed-document-node-plugin/CHANGELOG.json b/packages/graphql-codegen-supermassive-typed-document-node-plugin/CHANGELOG.json index 946a34e0f..a2e4be16d 100644 --- a/packages/graphql-codegen-supermassive-typed-document-node-plugin/CHANGELOG.json +++ b/packages/graphql-codegen-supermassive-typed-document-node-plugin/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@graphitation/graphql-codegen-supermassive-typed-document-node-plugin", "entries": [ + { + "date": "Wed, 30 Oct 2024 09:41:18 GMT", + "version": "1.0.8", + "tag": "@graphitation/graphql-codegen-supermassive-typed-document-node-plugin_v1.0.8", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@graphitation/graphql-codegen-supermassive-typed-document-node-plugin", + "comment": "Bump @graphitation/supermassive to v3.6.3", + "commit": "not available" + } + ] + } + }, { "date": "Thu, 24 Oct 2024 13:15:11 GMT", "version": "1.0.7", diff --git a/packages/graphql-codegen-supermassive-typed-document-node-plugin/CHANGELOG.md b/packages/graphql-codegen-supermassive-typed-document-node-plugin/CHANGELOG.md index 7a2742f96..2f5cf6d82 100644 --- a/packages/graphql-codegen-supermassive-typed-document-node-plugin/CHANGELOG.md +++ b/packages/graphql-codegen-supermassive-typed-document-node-plugin/CHANGELOG.md @@ -1,9 +1,17 @@ # Change Log - @graphitation/graphql-codegen-supermassive-typed-document-node-plugin - + +## 1.0.8 + +Wed, 30 Oct 2024 09:41:18 GMT + +### Patches + +- Bump @graphitation/supermassive to v3.6.3 + ## 1.0.7 Thu, 24 Oct 2024 13:15:11 GMT diff --git a/packages/graphql-codegen-supermassive-typed-document-node-plugin/package.json b/packages/graphql-codegen-supermassive-typed-document-node-plugin/package.json index 5fcb45c0a..0290227a2 100644 --- a/packages/graphql-codegen-supermassive-typed-document-node-plugin/package.json +++ b/packages/graphql-codegen-supermassive-typed-document-node-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@graphitation/graphql-codegen-supermassive-typed-document-node-plugin", "license": "MIT", - "version": "1.0.7", + "version": "1.0.8", "main": "./src/index.ts", "repository": { "type": "git", @@ -29,7 +29,7 @@ "@graphql-codegen/visitor-plugin-common": ">= ^1.17.0 < 2", "graphql-tag": ">= 2.11.0 < 3", "@graphql-tools/optimize": "^1.0.1", - "@graphitation/supermassive": "^3.6.2" + "@graphitation/supermassive": "^3.6.3" }, "sideEffects": false, "access": "public", diff --git a/packages/supermassive/package.json b/packages/supermassive/package.json index db5eeca8c..60e85e88c 100644 --- a/packages/supermassive/package.json +++ b/packages/supermassive/package.json @@ -1,7 +1,7 @@ { "name": "@graphitation/supermassive", "license": "MIT", - "version": "3.6.2", + "version": "3.6.3", "main": "./src/index.ts", "repository": { "type": "git", diff --git a/packages/supermassive/src/__tests__/hooks.test.ts b/packages/supermassive/src/__tests__/hooks.test.ts index 33d20955a..65a1c067f 100644 --- a/packages/supermassive/src/__tests__/hooks.test.ts +++ b/packages/supermassive/src/__tests__/hooks.test.ts @@ -108,28 +108,7 @@ describe.each([ // BOE: beforeOperationExecute // BSE: beforeSubscriptionEventEmit // ABR: afterBuildResponse - const hooks: ExecutionHooks = { - beforeOperationExecute: jest - .fn() - .mockImplementation( - ({ operation }: BaseExecuteOperationHookArgs) => { - hookCalls.push(`BOE|${operation.name?.value}`); - }, - ), - beforeSubscriptionEventEmit: jest - .fn() - .mockImplementation( - ({ - operation, - eventPayload, - }: BeforeSubscriptionEventEmitHookArgs) => { - hookCalls.push( - `BSE|${operation.name?.value}|${ - (eventPayload as any).emitPersons.name - }`, - ); - }, - ), + const syncAfterHooks: ExecutionHooks = { afterBuildResponse: jest .fn() .mockImplementation( @@ -137,13 +116,6 @@ describe.each([ hookCalls.push(`ABR|${operation.name?.value}`); }, ), - beforeFieldResolve: jest - .fn() - .mockImplementation( - ({ resolveInfo }: BaseExecuteFieldHookArgs) => { - hookCalls.push(`BFR|${pathToArray(resolveInfo.path).join(".")}`); - }, - ), afterFieldResolve: jest .fn() .mockImplementation( @@ -186,6 +158,68 @@ describe.each([ ), }; + const syncBeforeHooks: ExecutionHooks = { + beforeOperationExecute: jest + .fn() + .mockImplementation( + ({ operation }: BaseExecuteOperationHookArgs) => { + hookCalls.push(`BOE|${operation.name?.value}`); + }, + ), + beforeSubscriptionEventEmit: jest + .fn() + .mockImplementation( + ({ + operation, + eventPayload, + }: BeforeSubscriptionEventEmitHookArgs) => { + hookCalls.push( + `BSE|${operation.name?.value}|${ + (eventPayload as any).emitPersons.name + }`, + ); + }, + ), + beforeFieldResolve: jest + .fn() + .mockImplementation( + ({ resolveInfo }: BaseExecuteFieldHookArgs) => { + hookCalls.push(`BFR|${pathToArray(resolveInfo.path).join(".")}`); + }, + ), + }; + + const asyncBeforeHooks: ExecutionHooks = { + beforeOperationExecute: jest + .fn() + .mockImplementation( + async ({ operation }: BaseExecuteOperationHookArgs) => { + hookCalls.push(`BOE|${operation.name?.value}`); + }, + ), + beforeSubscriptionEventEmit: jest + .fn() + .mockImplementation( + async ({ + operation, + eventPayload, + }: BeforeSubscriptionEventEmitHookArgs) => { + hookCalls.push( + `BSE|${operation.name?.value}|${ + (eventPayload as any).emitPersons.name + }`, + ); + }, + ), + beforeFieldResolve: jest + .fn() + .mockImplementation( + async ({ resolveInfo }: BaseExecuteFieldHookArgs) => { + hookCalls.push(`BFR|${pathToArray(resolveInfo.path).join(".")}`); + }, + ), + }; + beforeEach(() => { jest.clearAllMocks(); hookCalls = []; @@ -520,6 +554,374 @@ describe.each([ }, ]; + const asyncHooksTestCases: Array = [ + { + name: "succeeded sync resolver with async hooks", + document: `query GetPerson + { + person(id: 1) { + name + } + }`, + resolvers: { + ...resolvers, + Person: { + name: (parent: any, _args: unknown, _context: any) => { + return parent.name; + }, + }, + } as UserResolvers, + expectedHookCalls: [ + "BOE|GetPerson", + "BFR|person", + "AFR|person|[object]|undefined", + "BFR|person.name", + "AFR|person.name|Luke Skywalker|undefined", + "AFC|person.name|Luke Skywalker|undefined", + "AFC|person|[object]|undefined", + "ABR|GetPerson", + ], + resultHasErrors: false, + isStrictHookCallsOrder: true, + }, + { + name: "succeeded async resolver with async hooks", + document: `query GetPerson + { + person(id: 1) { + name + } + }`, + resolvers: { + ...resolvers, + Person: { + name: async (parent: any, _args: unknown, _context: any) => { + return Promise.resolve(parent.name); + }, + }, + } as UserResolvers, + expectedHookCalls: [ + "BOE|GetPerson", + "BFR|person", + "AFR|person|[object]|undefined", + "BFR|person.name", + "AFR|person.name|Luke Skywalker|undefined", + "AFC|person.name|Luke Skywalker|undefined", + "AFC|person|[object]|undefined", + "ABR|GetPerson", + ], + resultHasErrors: false, + isStrictHookCallsOrder: false, + }, + { + name: "error in sync resolver for nullable field with async hooks", + document: `query GetFilm + { + film(id: 1) { + producer + } + }`, + resolvers: { + ...resolvers, + Film: { + producer: (_parent: any, _args: unknown, _context: any) => { + throw new Error("Resolver error"); + }, + }, + } as UserResolvers, + expectedHookCalls: [ + "BOE|GetFilm", + "BFR|film", + "AFR|film|[object]|undefined", + "BFR|film.producer", + "AFR|film.producer|undefined|Resolver error", + "AFC|film.producer|undefined|Resolver error", + "AFC|film|[object]|undefined", + "ABR|GetFilm", + ], + resultHasErrors: true, + isStrictHookCallsOrder: true, + }, + { + name: "error in async resolver for nullable field with async hooks", + document: `query GetFilm + { + film(id: 1) { + producer + } + }`, + resolvers: { + ...resolvers, + Film: { + producer: async (_parent: any, _args: unknown, _context: any) => { + return Promise.reject(new Error("Resolver error")); + }, + }, + } as UserResolvers, + expectedHookCalls: [ + "BOE|GetFilm", + "BFR|film", + "AFR|film|[object]|undefined", + "BFR|film.producer", + "AFR|film.producer|undefined|Resolver error", + "AFC|film.producer|undefined|Resolver error", + "AFC|film|[object]|undefined", + "ABR|GetFilm", + ], + resultHasErrors: true, + isStrictHookCallsOrder: false, + }, + { + name: "error in sync resolver for non-nullable field with async hooks", + document: `query GetFilm + { + film(id: 1) { + title + } + }`, + resolvers: { + ...resolvers, + Film: { + title: (_parent: any, _args: unknown, _context: any) => { + throw new Error("Resolver error"); + }, + }, + } as UserResolvers, + expectedHookCalls: [ + "BOE|GetFilm", + "BFR|film", + "AFR|film|[object]|undefined", + "BFR|film.title", + "AFR|film.title|undefined|Resolver error", + "AFC|film.title|undefined|Resolver error", + "AFC|film|undefined|Resolver error", + "ABR|GetFilm", + ], + resultHasErrors: true, + isStrictHookCallsOrder: true, + }, + { + name: "error in async resolver for non-nullable field with async hooks", + document: `query GetFilm + { + film(id: 1) { + title + } + }`, + resolvers: { + ...resolvers, + Film: { + title: async (_parent: any, _args: unknown, _context: any) => { + return Promise.reject(new Error("Resolver error")); + }, + }, + } as UserResolvers, + expectedHookCalls: [ + "BOE|GetFilm", + "BFR|film", + "AFR|film|[object]|undefined", + "BFR|film.title", + "AFR|film.title|undefined|Resolver error", + "AFC|film.title|undefined|Resolver error", + "AFC|film|undefined|Resolver error", + "ABR|GetFilm", + ], + resultHasErrors: true, + isStrictHookCallsOrder: false, + }, + { + name: "do not invoke hooks for the field with default resolver with async hooks", + document: `query GetFilm + { + film(id: 1) { + title + } + }`, + resolvers: resolvers as UserResolvers, + expectedHookCalls: [ + "BOE|GetFilm", + "BFR|film", + "AFR|film|[object]|undefined", + "AFC|film|[object]|undefined", + "ABR|GetFilm", + ], + resultHasErrors: false, + isStrictHookCallsOrder: true, + }, + { + name: "do not invoke hooks for the __typename with async hooks", + document: `query GetFilm + { + film(id: 1) { + __typename + title + } + }`, + resolvers: resolvers as UserResolvers, + expectedHookCalls: [ + "BOE|GetFilm", + "BFR|film", + "AFR|film|[object]|undefined", + "AFC|film|[object]|undefined", + "ABR|GetFilm", + ], + resultHasErrors: false, + isStrictHookCallsOrder: true, + }, + { + name: "multiple root fields in selection set with async hooks", + document: `query GetFilmAndPerson + { + film(id: 1) { + title + } + person(id: 1) { + name + } + }`, + resolvers: resolvers as UserResolvers, + expectedHookCalls: [ + "BOE|GetFilmAndPerson", + "BFR|film", + "BFR|person", + "AFR|film|[object]|undefined", + "AFR|person|[object]|undefined", + "AFC|film|[object]|undefined", + "AFC|person|[object]|undefined", + "ABR|GetFilmAndPerson", + ], + resultHasErrors: false, + isStrictHookCallsOrder: true, + }, + { + name: "subscription hooks", + document: `subscription EmitPersons($limit: Int!) + { + emitPersons(limit: $limit) { + name + } + }`, + variables: { + limit: 3, + }, + resolvers: resolvers as UserResolvers, + expectedHookCalls: [ + "BOE|EmitPersons", + "BFR|emitPersons", + "AFR|emitPersons|[object]|undefined", + "BSE|EmitPersons|Luke Skywalker", + "ABR|EmitPersons", + "BSE|EmitPersons|C-3PO", + "ABR|EmitPersons", + "BSE|EmitPersons|R2-D2", + "ABR|EmitPersons", + ], + resultHasErrors: false, + isStrictHookCallsOrder: true, + }, + { + name: "error in sync subscribe() resolver with async hooks", + document: `subscription EmitPersons($limit: Int!) + { + emitPersons(limit: $limit) { + name + } + }`, + resolvers: { + ...resolvers, + Subscription: { + emitPersons: { + subscribe: (_parent: any, _args: unknown, _context: any) => { + throw new Error("Subscribe error"); + }, + }, + }, + } as UserResolvers, + variables: { + limit: 1, + }, + expectedHookCalls: [ + "BOE|EmitPersons", + "BFR|emitPersons", + "AFR|emitPersons|undefined|Subscribe error", + ], + resultHasErrors: true, + isStrictHookCallsOrder: true, + }, + { + name: "error in async subscribe() resolver with async hooks", + document: `subscription EmitPersons($limit: Int!) + { + emitPersons(limit: $limit) { + name + } + }`, + resolvers: { + ...resolvers, + Subscription: { + emitPersons: { + subscribe: async ( + _parent: any, + _args: unknown, + _context: any, + ) => { + return Promise.reject(new Error("Subscribe error")); + }, + }, + }, + } as UserResolvers, + variables: { + limit: 1, + }, + expectedHookCalls: [ + "BOE|EmitPersons", + "BFR|emitPersons", + "AFR|emitPersons|undefined|Subscribe error", + ], + resultHasErrors: true, + isStrictHookCallsOrder: true, + }, + ]; + + it.each(asyncHooksTestCases)( + "$name", + async ({ + document, + resolvers, + expectedHookCalls, + resultHasErrors, + isStrictHookCallsOrder, + variables, + }) => { + expect.assertions(4); + const parsedDocument = parse(document); + + const result = await drainExecution( + await execute( + parsedDocument, + resolvers, + { ...asyncBeforeHooks, ...syncAfterHooks }, + variables, + ), + ); + + if (isStrictHookCallsOrder) { + expect(hookCalls).toEqual(expectedHookCalls); + } else { + // for async resolvers order of resolving isn't strict, + // so just verify whether corresponding hook calls happened + expect(hookCalls).toEqual(expect.arrayContaining(expectedHookCalls)); + } + expect(hookCalls).toHaveLength(expectedHookCalls.length); + expect(isTotalExecutionResult(result as TotalExecutionResult)).toBe( + true, + ); + expect(((result as TotalExecutionResult).errors?.length ?? 0) > 0).toBe( + resultHasErrors, + ); + }, + ); + it.each(testCases)( "$name", async ({ @@ -534,7 +936,12 @@ describe.each([ const parsedDocument = parse(document); const result = await drainExecution( - await execute(parsedDocument, resolvers, hooks, variables), + await execute( + parsedDocument, + resolvers, + { ...syncAfterHooks, ...syncBeforeHooks }, + variables, + ), ); if (isStrictHookCallsOrder) { @@ -553,6 +960,60 @@ describe.each([ ); }, ); + + test("BFR returns promise conditionally", async () => { + const result = await drainExecution( + await execute( + parse(`query GetFilmAndPerson + { + film(id: 1) { + title + } + person(id: 1) { + name + } + }`), + resolvers as UserResolvers, + { + ...asyncBeforeHooks, + ...syncAfterHooks, + beforeFieldResolve: jest + .fn() + .mockImplementation( + ({ resolveInfo }: BaseExecuteFieldHookArgs) => { + hookCalls.push( + `BFR|${pathToArray(resolveInfo.path).join(".")}`, + ); + if (resolveInfo.fieldName === "film") + return Promise.resolve(); + return; + }, + ), + }, + { + limit: 1, + }, + ), + ); + + const expectedHookCalls = [ + "BOE|GetFilmAndPerson", + "BFR|film", + "BFR|person", + "AFR|person|[object]|undefined", + "AFC|person|[object]|undefined", + "AFR|film|[object]|undefined", + "AFC|film|[object]|undefined", + "ABR|GetFilmAndPerson", + ]; + expect(hookCalls).toEqual(expectedHookCalls); + + expect(hookCalls).toHaveLength(expectedHookCalls.length); + expect(isTotalExecutionResult(result as TotalExecutionResult)).toBe(true); + expect(((result as TotalExecutionResult).errors?.length ?? 0) > 0).toBe( + false, + ); + }); }); describe("Error thrown in the hook doesn't break execution and is returned in response 'errors'", () => { @@ -759,6 +1220,48 @@ describe.each([ expectedErrorMessage: 'Unexpected error in beforeSubscriptionEventEmit hook: "Hook error"', }, + { + name: "async beforeSubscriptionEventEmit (Error is thrown)", + document: `subscription EmitPersons($limit: Int!) + { + emitPersons(limit: $limit) { + name + } + }`, + variables: { + limit: 1, + }, + hooks: { + beforeSubscriptionEventEmit: jest + .fn() + .mockImplementation(async () => { + throw new Error("Hook error"); + }), + }, + expectedErrorMessage: + "Unexpected error in beforeSubscriptionEventEmit hook: Hook error", + }, + { + name: "async beforeSubscriptionEventEmit (string is thrown)", + document: `subscription EmitPersons($limit: Int!) + { + emitPersons(limit: $limit) { + name + } + }`, + variables: { + limit: 1, + }, + hooks: { + beforeSubscriptionEventEmit: jest + .fn() + .mockImplementation(async () => { + throw "Hook error"; + }), + }, + expectedErrorMessage: + 'Unexpected error in beforeSubscriptionEventEmit hook: "Hook error"', + }, ]; it.each(testCases)( @@ -817,4 +1320,35 @@ describe.each([ expect.objectContaining({ hookContext: afterHookContext }), ); }); + + it('passes async "before" hook context but "after" hook should already receive resolved promise', async () => { + expect.assertions(2); + + const query = ` + { + film(id: 1) { + title + } + }`; + const beforeHookContext = { + foo: "foo", + }; + const afterHookContext = { + bar: "bar", + }; + const hooks: ExecutionHooks = { + beforeFieldResolve: jest.fn(async () => beforeHookContext), + afterFieldResolve: jest.fn(() => afterHookContext), + afterFieldComplete: jest.fn(), + }; + + await execute(parse(query), resolvers as UserResolvers, hooks); + + expect(hooks.afterFieldResolve).toHaveBeenCalledWith( + expect.objectContaining({ hookContext: beforeHookContext }), + ); + expect(hooks.afterFieldComplete).toHaveBeenCalledWith( + expect.objectContaining({ hookContext: afterHookContext }), + ); + }); }); diff --git a/packages/supermassive/src/executeWithoutSchema.ts b/packages/supermassive/src/executeWithoutSchema.ts index 4f67386af..a9bea6a09 100644 --- a/packages/supermassive/src/executeWithoutSchema.ts +++ b/packages/supermassive/src/executeWithoutSchema.ts @@ -144,7 +144,7 @@ export function executeWithoutSchema( if (!("schemaFragment" in exeContext)) { return { errors: exeContext }; } else { - return executeOperation(exeContext); + return executeOperationWithBeforeHook(exeContext); } } @@ -277,23 +277,35 @@ function buildPerEventExecutionContext( }; } +function executeOperationWithBeforeHook( + exeContext: ExecutionContext, +): PromiseOrValue { + const hooks = exeContext.fieldExecutionHooks; + let hook: Promise | void | undefined; + if (hooks?.beforeOperationExecute) { + hook = invokeBeforeOperationExecuteHook(exeContext); + } + + if (isPromise(hook)) { + return hook.then(() => executeOperation(exeContext)); + } + + return executeOperation(exeContext); +} + function executeOperation( exeContext: ExecutionContext, ): PromiseOrValue { try { const { operation, rootValue } = exeContext; const rootTypeName = getOperationRootTypeName(operation); - const { groupedFieldSet, patches } = collectFields( exeContext, rootTypeName, ); const path = undefined; let result; - const hooks = exeContext.fieldExecutionHooks; - if (hooks?.beforeOperationExecute) { - invokeBeforeOperationExecuteHook(exeContext); - } + // Note: cannot use OperationTypeNode from graphql-js as it doesn't exist in 15.x switch (operation.operation) { case "query": @@ -686,7 +698,12 @@ function executeSubscriptionImpl( // Call the `subscribe()` resolver or the default resolver to produce an // AsyncIterable yielding raw payloads. - const result = resolveFn(rootValue, args, contextValue, info); + const result = isPromise(hookContext) + ? hookContext.then((context) => { + hookContext = context; + return resolveFn(rootValue, args, contextValue, info); + }) + : resolveFn(rootValue, args, contextValue, info); if (isPromise(result)) { return result.then(assertEventStream).then( @@ -796,18 +813,33 @@ function mapResultOrEventStreamOrPromise( payload, ); const hooks = exeContext?.fieldExecutionHooks; + let beforeExecuteFieldsHook: void | Promise | undefined; if (hooks?.beforeSubscriptionEventEmit) { - invokeBeforeSubscriptionEventEmitHook(perEventContext, payload); - } - try { - const data = executeFields( - exeContext, - parentTypeName, + beforeExecuteFieldsHook = invokeBeforeSubscriptionEventEmitHook( + perEventContext, payload, - path, - groupedFieldSet, - undefined, ); + } + try { + const data = isPromise(beforeExecuteFieldsHook) + ? beforeExecuteFieldsHook.then(() => + executeFields( + exeContext, + parentTypeName, + payload, + path, + groupedFieldSet, + undefined, + ), + ) + : executeFields( + exeContext, + parentTypeName, + payload, + path, + groupedFieldSet, + undefined, + ); // This is typechecked in collect values return buildResponse(perEventContext, data) as TotalExecutionResult; } catch (error) { @@ -919,7 +951,12 @@ function resolveAndCompleteField( hookContext = invokeBeforeFieldResolveHook(info, exeContext); } - const result = resolveFn(source, args, contextValue, info); + const result = isPromise(hookContext) + ? hookContext.then((context) => { + hookContext = context; + return resolveFn(source, args, contextValue, info); + }) + : resolveFn(source, args, contextValue, info); let completed; if (isPromise(result)) { @@ -1944,19 +1981,34 @@ function invokeAfterBuildResponseHook( } function executeSafe( - execute: () => T, + execute: () => T | Promise, onComplete: (result: T | undefined, error: unknown) => void, -): T { +): T | Promise { let error: unknown; - let result: T | undefined; + let result: T | Promise | undefined; try { result = execute(); } catch (e) { error = e; } finally { - onComplete(result, error); + if (!isPromise(result)) { + onComplete(result, error); + } } - return result as T; + + if (!isPromise(result)) { + return result as T; + } + + return result + .then((hookResult) => { + onComplete(hookResult, error); + return hookResult; + }) + .catch((e) => { + onComplete(undefined, e); + return undefined; + }) as Promise; } function toGraphQLError( diff --git a/packages/supermassive/src/hooks/types.ts b/packages/supermassive/src/hooks/types.ts index 2197621ae..b41165c23 100644 --- a/packages/supermassive/src/hooks/types.ts +++ b/packages/supermassive/src/hooks/types.ts @@ -46,7 +46,9 @@ export interface BeforeFieldResolveHook< ResolveContext = unknown, BeforeHookContext = unknown, > { - (args: BaseExecuteFieldHookArgs): BeforeHookContext; + (args: BaseExecuteFieldHookArgs): + | Promise + | BeforeHookContext; } export interface AfterFieldResolveHook< @@ -71,11 +73,13 @@ export interface AfterBuildResponseHook { } export interface BeforeOperationExecuteHook { - (args: BaseExecuteOperationHookArgs): void; + (args: BaseExecuteOperationHookArgs): void | Promise; } export interface BeforeSubscriptionEventEmitHook { - (args: BeforeSubscriptionEventEmitHookArgs): void; + ( + args: BeforeSubscriptionEventEmitHookArgs, + ): void | Promise; } export interface ExecutionHooks< diff --git a/packages/ts-codegen/CHANGELOG.json b/packages/ts-codegen/CHANGELOG.json index 466d0383e..ff0d98add 100644 --- a/packages/ts-codegen/CHANGELOG.json +++ b/packages/ts-codegen/CHANGELOG.json @@ -1,6 +1,96 @@ { "name": "@graphitation/ts-codegen", "entries": [ + { + "date": "Thu, 14 Nov 2024 14:18:34 GMT", + "version": "2.13.0-alpha.6", + "tag": "@graphitation/ts-codegen_v2.13.0-alpha.6", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/ts-codegen", + "commit": "2c90bfecd577995d71baf009e376376d576a79b8", + "comment": "Interface inheritence logic cleanup, tests added and union fixed" + } + ] + } + }, + { + "date": "Wed, 13 Nov 2024 20:52:03 GMT", + "version": "2.13.0-alpha.5", + "tag": "@graphitation/ts-codegen_v2.13.0-alpha.5", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/ts-codegen", + "commit": "e9a34254f389d06042d7deb11bca819dd2d642ed", + "comment": "Solution cleanup - new cli parameters and inheritance fixes" + } + ] + } + }, + { + "date": "Mon, 11 Nov 2024 14:13:12 GMT", + "version": "2.13.0-alpha.4", + "tag": "@graphitation/ts-codegen_v2.13.0-alpha.4", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/ts-codegen", + "commit": "f3b1a34db0e9fc7bbf4ddf972fd509f4e7d9f5ac", + "comment": "Added import state machines templating" + } + ] + } + }, + { + "date": "Mon, 04 Nov 2024 17:34:20 GMT", + "version": "2.13.0-alpha.3", + "tag": "@graphitation/ts-codegen_v2.13.0-alpha.3", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/ts-codegen", + "commit": "dd33b80a0c6975c118d8376cbba61cda706802c7", + "comment": "Added context namespace into the codegen" + } + ] + } + }, + { + "date": "Wed, 30 Oct 2024 20:39:21 GMT", + "version": "2.13.0-alpha.2", + "tag": "@graphitation/ts-codegen_v2.13.0-alpha.2", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/ts-codegen", + "commit": "826804f7e05cb0b5385f3f1f0367381aba329c47", + "comment": "[HOTFIX] ts-codegen context metadata map returned" + } + ] + } + }, + { + "date": "Wed, 30 Oct 2024 09:41:18 GMT", + "version": "2.13.0-alpha.1", + "tag": "@graphitation/ts-codegen_v2.13.0-alpha.1", + "comments": { + "prerelease": [ + { + "author": "77059398+vejrj@users.noreply.github.com", + "package": "@graphitation/ts-codegen", + "commit": "d69740884c12998eb1fb43ce8dc46afc1a577956", + "comment": "metadata written to a file and tests fixed" + } + ] + } + }, { "date": "Thu, 24 Oct 2024 13:15:11 GMT", "version": "2.12.1", diff --git a/packages/ts-codegen/CHANGELOG.md b/packages/ts-codegen/CHANGELOG.md index 0d4e706cb..4f33cb1f9 100644 --- a/packages/ts-codegen/CHANGELOG.md +++ b/packages/ts-codegen/CHANGELOG.md @@ -1,9 +1,57 @@ # Change Log - @graphitation/ts-codegen - + +## 2.13.0-alpha.6 + +Thu, 14 Nov 2024 14:18:34 GMT + +### Changes + +- Interface inheritence logic cleanup, tests added and union fixed (77059398+vejrj@users.noreply.github.com) + +## 2.13.0-alpha.5 + +Wed, 13 Nov 2024 20:52:03 GMT + +### Changes + +- Solution cleanup - new cli parameters and inheritance fixes (77059398+vejrj@users.noreply.github.com) + +## 2.13.0-alpha.4 + +Mon, 11 Nov 2024 14:13:12 GMT + +### Changes + +- Added import state machines templating (77059398+vejrj@users.noreply.github.com) + +## 2.13.0-alpha.3 + +Mon, 04 Nov 2024 17:34:20 GMT + +### Changes + +- Added context namespace into the codegen (77059398+vejrj@users.noreply.github.com) + +## 2.13.0-alpha.2 + +Wed, 30 Oct 2024 20:39:21 GMT + +### Changes + +- [HOTFIX] ts-codegen context metadata map returned (77059398+vejrj@users.noreply.github.com) + +## 2.13.0-alpha.1 + +Wed, 30 Oct 2024 09:41:18 GMT + +### Changes + +- metadata written to a file and tests fixed (77059398+vejrj@users.noreply.github.com) + ## 2.12.1 Thu, 24 Oct 2024 13:15:11 GMT diff --git a/packages/ts-codegen/package.json b/packages/ts-codegen/package.json index 57acab373..8f30003db 100644 --- a/packages/ts-codegen/package.json +++ b/packages/ts-codegen/package.json @@ -1,7 +1,7 @@ { "name": "@graphitation/ts-codegen", "license": "MIT", - "version": "2.12.1", + "version": "2.13.0-alpha.6", "main": "./src/index.ts", "repository": { "type": "git", diff --git a/packages/ts-codegen/src/__tests__/context.test.ts b/packages/ts-codegen/src/__tests__/context.test.ts new file mode 100644 index 000000000..cfad75e9c --- /dev/null +++ b/packages/ts-codegen/src/__tests__/context.test.ts @@ -0,0 +1,888 @@ +import ts from "typescript"; +import { parse } from "graphql"; +import { blankGraphQLTag as graphql } from "../utilities"; +import { generateTS } from ".."; +import { ContextMap } from "../context"; + +describe(generateTS, () => { + describe("Tests basic syntax GraphQL syntax", () => { + test("all possible nullable and non-nullable combinations", () => { + const { resolvers, models, enums, inputs, contextMappingOutput } = + runGenerateTest( + graphql` + extend schema + @import(from: "@msteams/packages-test", defs: ["Avatar"]) + type Post + @model(from: "./post-model.interface", tsType: "PostModel") { + id: ID! + } + + type Message { + id: ID! @context(stateMachines: ["message"]) + } + + type User @context(stateMachines: ["user"]) { + id: ID! @context(stateMachines: ["id-user"]) + name: String + messagesWithAnswersNonRequired: [[Message]] + messagesWithAnswersRequired: [[Message]]! + messagesWithAnswersAllRequired: [[Message!]!]! + messagesNonRequired: [Message] + messagesWithArrayRequired: [Message]! + messagesRequired: [Message!]! + messagesOnlyMessageRequired: [Message!] + post: Post @context(stateMachines: ["post"]) + postRequired: Post! + avatar: Avatar + avatarRequired: Avatar! + } + + extend type Query { + requiredUsers: [User!]! + optionalUsers: [User] + optionalUser: User + requiredUser: User! + requiredPost: Post! + optionalPost: Post + } + `, + { + contextSubTypeNameTemplate: "I${contextName}StateMachineContext", + contextSubTypePathTemplate: "@msteams/core-cdl-sync-${contextName}", + }, + ); + expect(enums).toMatchInlineSnapshot(`undefined`); + expect(contextMappingOutput).toMatchInlineSnapshot(` + { + "Message": { + "id": [ + "message", + ], + }, + "User": { + "__context": [ + "user", + ], + "id": [ + "id-user", + ], + "post": [ + "post", + ], + }, + } + `); + expect(inputs).toMatchInlineSnapshot(`undefined`); + expect(models).toMatchInlineSnapshot(` + "import type { Avatar } from "@msteams/packages-test"; + import type { PostModel as _Post } from "../post-model.interface"; + // Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) + export interface BaseModel { + readonly __typename?: string; + } + export interface Post extends BaseModel, _Post { + readonly __typename?: "Post"; + } + export interface Message extends BaseModel { + readonly __typename?: "Message"; + readonly id: string; + } + export interface User extends BaseModel { + readonly __typename?: "User"; + readonly id: string; + readonly name?: string | null; + readonly messagesWithAnswersNonRequired?: ReadonlyArray | null> | null; + readonly messagesWithAnswersRequired: ReadonlyArray | null>; + readonly messagesWithAnswersAllRequired: ReadonlyArray>; + readonly messagesNonRequired?: ReadonlyArray | null; + readonly messagesWithArrayRequired: ReadonlyArray; + readonly messagesRequired: ReadonlyArray; + readonly messagesOnlyMessageRequired?: ReadonlyArray | null; + readonly post?: Post | null; + readonly postRequired: Post; + readonly avatar?: Avatar | null; + readonly avatarRequired: Avatar; + } + " + `); + expect(resolvers).toMatchInlineSnapshot(` + "import type { Avatar } from "@msteams/packages-test"; + import type { PromiseOrValue } from "@graphitation/supermassive"; + import type { ResolveInfo } from "@graphitation/supermassive"; + import * as Models from "./models.interface"; + import type { IMessageStateMachineContext } from "@msteams/core-cdl-sync-message"; + import type { IUserStateMachineContext } from "@msteams/core-cdl-sync-user"; + import type { IIdUserStateMachineContext } from "@msteams/core-cdl-sync-id-user"; + import type { IPostStateMachineContext } from "@msteams/core-cdl-sync-post"; + export declare namespace Post { + export interface Resolvers { + readonly id?: id; + } + export type id = (model: Models.Post, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Message { + export interface Resolvers { + readonly id?: id; + } + export type id = (model: Models.Message, args: {}, context: IMessageStateMachineContext, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace User { + export interface Resolvers { + readonly id?: id; + readonly name?: name; + readonly messagesWithAnswersNonRequired?: messagesWithAnswersNonRequired; + readonly messagesWithAnswersRequired?: messagesWithAnswersRequired; + readonly messagesWithAnswersAllRequired?: messagesWithAnswersAllRequired; + readonly messagesNonRequired?: messagesNonRequired; + readonly messagesWithArrayRequired?: messagesWithArrayRequired; + readonly messagesRequired?: messagesRequired; + readonly messagesOnlyMessageRequired?: messagesOnlyMessageRequired; + readonly post?: post; + readonly postRequired?: postRequired; + readonly avatar?: avatar; + readonly avatarRequired?: avatarRequired; + } + export type id = (model: Models.User, args: {}, context: IIdUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type name = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type messagesWithAnswersNonRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue | null | undefined> | null | undefined>; + export type messagesWithAnswersRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue | null | undefined>>; + export type messagesWithAnswersAllRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue>>; + export type messagesNonRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue | null | undefined>; + export type messagesWithArrayRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue>; + export type messagesRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue>; + export type messagesOnlyMessageRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue | null | undefined>; + export type post = (model: Models.User, args: {}, context: IPostStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type postRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type avatar = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type avatarRequired = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Query { + export interface Resolvers { + readonly requiredUsers?: requiredUsers; + readonly optionalUsers?: optionalUsers; + readonly optionalUser?: optionalUser; + readonly requiredUser?: requiredUser; + readonly requiredPost?: requiredPost; + readonly optionalPost?: optionalPost; + } + export type requiredUsers = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue>; + export type optionalUsers = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue | null | undefined>; + export type optionalUser = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + export type requiredUser = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + export type requiredPost = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + export type optionalPost = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + " + `); + }); + test("Subscription", () => { + const { resolvers, models, enums, inputs, contextMappingOutput } = + runGenerateTest(graphql` + type User { + id: ID! + } + + extend type Subscription { + userUpdated: User! + } + `); + expect(enums).toMatchInlineSnapshot(`undefined`); + expect(contextMappingOutput).toMatchInlineSnapshot(`{}`); + expect(inputs).toMatchInlineSnapshot(`undefined`); + expect(models).toMatchInlineSnapshot(` + "// Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) + export interface BaseModel { + readonly __typename?: string; + } + export interface User extends BaseModel { + readonly __typename?: "User"; + readonly id: string; + } + " + `); + expect(resolvers).toMatchInlineSnapshot(` + "import type { PromiseOrValue } from "@graphitation/supermassive"; + import type { ResolveInfo } from "@graphitation/supermassive"; + import * as Models from "./models.interface"; + export declare namespace User { + export interface Resolvers { + readonly id?: id; + } + export type id = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Subscription { + export interface Resolvers { + readonly userUpdated?: userUpdated; + } + export type userUpdated = { + subscribe: (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue>; + } | { + subscribe: (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue>; + resolve: (subcribeResult: SubscribeResult, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + }; + } + " + `); + }); + test("Subscription with model", () => { + const { resolvers } = runGenerateTest(graphql` + type User @model(from: "./user-model.interface", tsType: "UserModel") { + id: ID! + } + + extend type Subscription { + userUpdated: User! + } + `); + expect(resolvers).toMatchInlineSnapshot(` + "import type { PromiseOrValue } from "@graphitation/supermassive"; + import type { ResolveInfo } from "@graphitation/supermassive"; + import * as Models from "./models.interface"; + export declare namespace User { + export interface Resolvers { + readonly id?: id; + } + export type id = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Subscription { + export interface Resolvers { + readonly userUpdated?: userUpdated; + } + export type userUpdated = { + subscribe: (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue>; + } | { + subscribe: (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue>; + resolve: (subcribeResult: SubscribeResult, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + }; + } + " + `); + }); + test("extends by exteding a type with pre-generated BaseModel type", () => { + const { resolvers, models, enums, inputs, contextMappingOutput } = + runGenerateTest(graphql` + type User { + id: ID! + } + + extend type Query { + users: [User!]! + } + `); + expect(enums).toMatchInlineSnapshot(`undefined`); + expect(contextMappingOutput).toMatchInlineSnapshot(`{}`); + expect(inputs).toMatchInlineSnapshot(`undefined`); + expect(models).toMatchInlineSnapshot(` + "// Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) + export interface BaseModel { + readonly __typename?: string; + } + export interface User extends BaseModel { + readonly __typename?: "User"; + readonly id: string; + } + " + `); + expect(resolvers).toMatchInlineSnapshot(` + "import type { PromiseOrValue } from "@graphitation/supermassive"; + import type { ResolveInfo } from "@graphitation/supermassive"; + import * as Models from "./models.interface"; + export declare namespace User { + export interface Resolvers { + readonly id?: id; + } + export type id = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Query { + export interface Resolvers { + readonly users?: users; + } + export type users = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue>; + } + " + `); + }); + test("case when interface implements multiple interfaces", () => { + const { resolvers, models, enums, inputs, contextMappingOutput } = + runGenerateTest( + graphql` + interface Node @context(stateMachines: ["node"]) { + id: ID! + } + + interface Persona @context(stateMachines: ["persona"]) { + phone: String! + } + + interface User implements Node & Persona { + id: ID! + name: String! + } + + type Admin implements Node & Persona + @context(stateMachines: ["admin"]) { + id: ID! + rank: Int! + } + + extend type Query { + users: [User] + admins: [Admin] + } + `, + { + contextSubTypeNameTemplate: "I${contextName}StateMachineContext", + contextSubTypePathTemplate: "@msteams/core-cdl-sync-${contextName}", + }, + ); + expect(enums).toMatchInlineSnapshot(`undefined`); + expect(contextMappingOutput).toMatchInlineSnapshot(` + { + "Admin": { + "__context": [ + "admin", + ], + }, + "Node": { + "__context": [ + "node", + ], + }, + "Persona": { + "__context": [ + "persona", + ], + }, + } + `); + expect(inputs).toMatchInlineSnapshot(`undefined`); + expect(models).toMatchInlineSnapshot(` + "// Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) + export interface BaseModel { + readonly __typename?: string; + } + export interface Node extends BaseModel { + readonly __typename?: string; + } + export interface Persona extends BaseModel { + readonly __typename?: string; + } + export interface User extends BaseModel, Node, Persona { + readonly __typename?: string; + } + export interface Admin extends BaseModel, Node, Persona { + readonly __typename?: "Admin"; + readonly id: string; + readonly rank: number; + } + " + `); + expect(resolvers).toMatchInlineSnapshot(` + "import type { PromiseOrValue } from "@graphitation/supermassive"; + import type { ResolveInfo } from "@graphitation/supermassive"; + import * as Models from "./models.interface"; + import type { INodeStateMachineContext } from "@msteams/core-cdl-sync-node"; + import type { IPersonaStateMachineContext } from "@msteams/core-cdl-sync-persona"; + import type { IAdminStateMachineContext } from "@msteams/core-cdl-sync-admin"; + export declare namespace Node { + export interface Resolvers { + readonly __resolveType?: __resolveType; + } + export type __resolveType = (parent: unknown, context: INodeStateMachineContext, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Persona { + export interface Resolvers { + readonly __resolveType?: __resolveType; + } + export type __resolveType = (parent: unknown, context: IPersonaStateMachineContext, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace User { + export interface Resolvers { + readonly __resolveType?: __resolveType; + } + export type __resolveType = (parent: unknown, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Admin { + export interface Resolvers { + readonly id?: id; + readonly rank?: rank; + } + export type id = (model: Models.Admin, args: {}, context: IAdminStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type rank = (model: Models.Admin, args: {}, context: IAdminStateMachineContext, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Query { + export interface Resolvers { + readonly users?: users; + readonly admins?: admins; + } + export type users = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue | null | undefined>; + export type admins = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue | null | undefined>; + } + " + `); + }); + test("extensions are not generated in the models", () => { + const { models } = runGenerateTest(graphql` + extend schema @import(from: "@msteams/packages-test", defs: ["User"]) + + extend type User { + id: ID! + name: String! + } + + type Post { + id: ID! + user: User! + } + `); + expect(models).toMatchInlineSnapshot(` + "import type { User } from "@msteams/packages-test"; + // Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) + export interface BaseModel { + readonly __typename?: string; + } + export interface Post extends BaseModel { + readonly __typename?: "Post"; + readonly id: string; + readonly user: User; + } + " + `); + }); + + test("implements -> @context in interfaces should be used only in resolveType", () => { + const { resolvers, models, enums, inputs, contextMappingOutput } = + runGenerateTest( + graphql` + interface Node @context(stateMachines: ["node"]) { + id: ID! + } + + interface Customer implements Node + @context(stateMachines: ["customer"]) { + id: ID! + name: String! + } + + type User implements Node & Customer { + id: ID! + name: String! + } + + extend type Query { + users: [User] + } + `, + { + contextSubTypeNameTemplate: "I${contextName}StateMachineContext", + contextSubTypePathTemplate: "@msteams/core-cdl-sync-${contextName}", + }, + ); + expect(enums).toMatchInlineSnapshot(`undefined`); + expect(contextMappingOutput).toMatchInlineSnapshot(` + { + "Customer": { + "__context": [ + "customer", + ], + }, + "Node": { + "__context": [ + "node", + ], + }, + } + `); + expect(inputs).toMatchInlineSnapshot(`undefined`); + expect(models).toMatchInlineSnapshot(` + "// Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) + export interface BaseModel { + readonly __typename?: string; + } + export interface Node extends BaseModel { + readonly __typename?: string; + } + export interface Customer extends BaseModel, Node { + readonly __typename?: string; + } + export interface User extends BaseModel, Node, Customer { + readonly __typename?: "User"; + readonly id: string; + readonly name: string; + } + " + `); + expect(resolvers).toMatchInlineSnapshot(` + "import type { PromiseOrValue } from "@graphitation/supermassive"; + import type { ResolveInfo } from "@graphitation/supermassive"; + import * as Models from "./models.interface"; + import type { INodeStateMachineContext } from "@msteams/core-cdl-sync-node"; + import type { ICustomerStateMachineContext } from "@msteams/core-cdl-sync-customer"; + export declare namespace Node { + export interface Resolvers { + readonly __resolveType?: __resolveType; + } + export type __resolveType = (parent: unknown, context: INodeStateMachineContext, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Customer { + export interface Resolvers { + readonly __resolveType?: __resolveType; + } + export type __resolveType = (parent: unknown, context: ICustomerStateMachineContext, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace User { + export interface Resolvers { + readonly id?: id; + readonly name?: name; + } + export type id = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + export type name = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Query { + export interface Resolvers { + readonly users?: users; + } + export type users = (model: unknown, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue | null | undefined>; + } + " + `); + }); + + test("applying @context to enum shouldn't affect anything", () => { + const { resolvers, models, enums, inputs, contextMappingOutput } = + runGenerateTest(graphql` + enum PresenceAvailability + @context(stateMachines: ["shouldnt-apply"]) { + Available + Away + Offline + } + + type User { + id: ID! + availability: PresenceAvailability! + } + + extend type Query { + userById(id: ID!): User + } + `); + expect(enums).toMatchInlineSnapshot(` + "export enum PresenceAvailability { + Available = "Available", + Away = "Away", + Offline = "Offline" + } + " + `); + expect(inputs).toMatchInlineSnapshot(`undefined`); + expect(contextMappingOutput).toMatchInlineSnapshot(`{}`); + expect(models).toMatchInlineSnapshot(` + "import * as Enums from "./enums.interface"; + export * from "./enums.interface"; + // Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) + export interface BaseModel { + readonly __typename?: string; + } + export interface User extends BaseModel { + readonly __typename?: "User"; + readonly id: string; + readonly availability: Enums.PresenceAvailability; + } + " + `); + expect(resolvers).toMatchInlineSnapshot(` + "import type { PromiseOrValue } from "@graphitation/supermassive"; + import type { ResolveInfo } from "@graphitation/supermassive"; + import * as Models from "./models.interface"; + export declare namespace User { + export interface Resolvers { + readonly id?: id; + readonly availability?: availability; + } + export type id = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + export type availability = (model: Models.User, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Query { + export interface Resolvers { + readonly userById?: userById; + } + export type userById = (model: unknown, args: { + readonly id: string; + }, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + " + `); + }); + + test("Union and interface types", () => { + const { resolvers, models, enums, inputs, contextMappingOutput } = + runGenerateTest( + graphql` + type Customer { + id: ID! + } + + type Company { + id: ID! + } + + type Admin @context(stateMachines: ["admin"]) { + id: ID! + } + + type User @context(stateMachines: ["user"]) { + id: ID! + } + + interface Node { + id: ID! + } + + union UserOrAdmin = User | Admin + union UserOrCustomer @context(stateMachines: ["user-or-customer"]) = + User + | Customer + union CompanyOrCustomer + @context(stateMachines: ["company-or-customer"]) = + Company + | Customer + + extend type Query { + userById(id: ID!): whatever @context(stateMachines: ["whatever"]) + userByMail(mail: String): whatever + @context(stateMachines: ["different-whatever"]) + node(id: ID!): Node + } + `, + { + contextSubTypeNameTemplate: "I${contextName}StateMachineContext", + contextSubTypePathTemplate: "@msteams/core-cdl-sync-${contextName}", + }, + ); + expect(enums).toMatchInlineSnapshot(`undefined`); + expect(contextMappingOutput).toMatchInlineSnapshot(` + { + "Admin": { + "__context": [ + "admin", + ], + }, + "CompanyOrCustomer": { + "__context": [ + "company-or-customer", + ], + }, + "Query": { + "userById": [ + "whatever", + ], + "userByMail": [ + "different-whatever", + ], + }, + "User": { + "__context": [ + "user", + ], + }, + "UserOrCustomer": { + "__context": [ + "user-or-customer", + ], + }, + } + `); + expect(inputs).toMatchInlineSnapshot(`undefined`); + expect(models).toMatchInlineSnapshot(` + "// Base type for all models. Enables automatic resolution of abstract GraphQL types (interfaces, unions) + export interface BaseModel { + readonly __typename?: string; + } + export interface Customer extends BaseModel { + readonly __typename?: "Customer"; + readonly id: string; + } + export interface Company extends BaseModel { + readonly __typename?: "Company"; + readonly id: string; + } + export interface Admin extends BaseModel { + readonly __typename?: "Admin"; + readonly id: string; + } + export interface User extends BaseModel { + readonly __typename?: "User"; + readonly id: string; + } + export interface Node extends BaseModel { + readonly __typename?: string; + } + export type UserOrAdmin = User | Admin; + export type UserOrCustomer = User | Customer; + export type CompanyOrCustomer = Company | Customer; + " + `); + expect(resolvers).toMatchInlineSnapshot(` + "import type { PromiseOrValue } from "@graphitation/supermassive"; + import type { ResolveInfo } from "@graphitation/supermassive"; + import * as Models from "./models.interface"; + import type { IAdminStateMachineContext } from "@msteams/core-cdl-sync-admin"; + import type { IUserStateMachineContext } from "@msteams/core-cdl-sync-user"; + import type { IUserOrCustomerStateMachineContext } from "@msteams/core-cdl-sync-user-or-customer"; + import type { ICompanyOrCustomerStateMachineContext } from "@msteams/core-cdl-sync-company-or-customer"; + import type { IWhateverStateMachineContext } from "@msteams/core-cdl-sync-whatever"; + import type { IDifferentWhateverStateMachineContext } from "@msteams/core-cdl-sync-different-whatever"; + export declare namespace Customer { + export interface Resolvers { + readonly id?: id; + } + export type id = (model: Models.Customer, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Company { + export interface Resolvers { + readonly id?: id; + } + export type id = (model: Models.Company, args: {}, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Admin { + export interface Resolvers { + readonly id?: id; + } + export type id = (model: Models.Admin, args: {}, context: IAdminStateMachineContext, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace User { + export interface Resolvers { + readonly id?: id; + } + export type id = (model: Models.User, args: {}, context: IUserStateMachineContext, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace Node { + export interface Resolvers { + readonly __resolveType?: __resolveType; + } + export type __resolveType = (parent: unknown, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + export declare namespace UserOrAdmin { + export interface Resolvers { + readonly __resolveType?: __resolveType; + } + export type __resolveType = (parent: Models.User | Models.Admin, context: unknown, info: ResolveInfo) => PromiseOrValue<"User" | "Admin" | null>; + } + export declare namespace UserOrCustomer { + export interface Resolvers { + readonly __resolveType?: __resolveType; + } + export type __resolveType = (parent: Models.User | Models.Customer, context: IUserOrCustomerStateMachineContext, info: ResolveInfo) => PromiseOrValue<"User" | "Customer" | null>; + } + export declare namespace CompanyOrCustomer { + export interface Resolvers { + readonly __resolveType?: __resolveType; + } + export type __resolveType = (parent: Models.Company | Models.Customer, context: ICompanyOrCustomerStateMachineContext, info: ResolveInfo) => PromiseOrValue<"Company" | "Customer" | null>; + } + export declare namespace Query { + export interface Resolvers { + readonly userById?: userById; + readonly userByMail?: userByMail; + readonly node?: node; + } + export type userById = (model: unknown, args: { + readonly id: string; + }, context: IWhateverStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type userByMail = (model: unknown, args: { + readonly mail?: string | null; + }, context: IDifferentWhateverStateMachineContext, info: ResolveInfo) => PromiseOrValue; + export type node = (model: unknown, args: { + readonly id: string; + }, context: unknown, info: ResolveInfo) => PromiseOrValue; + } + " + `); + }); + }); +}); + +function runGenerateTest( + doc: string, + options: { + outputPath?: string; + documentPath?: string; + defaultContextTypePath?: string; + contextName?: string; + legacyCompat?: boolean; + enumsImport?: string; + legacyNoModelsForObjects?: boolean; + legacyEnumsCompatibility?: boolean; + useStringUnionsInsteadOfEnums?: boolean; + enumNamesToMigrate?: string[]; + enumNamesToKeep?: string[]; + modelScope?: string; + contextSubTypeNameTemplate?: string; + contextSubTypePathTemplate?: string; + } = {}, +): { + enums?: string; + inputs?: string; + models: string; + resolvers: string; + legacyTypes?: string; + legacyResolvers?: string; + legacyNoModelsForObjects?: boolean; + legacyEnumsCompatibility?: boolean; + useStringUnionsInsteadOfEnums?: boolean; + enumNamesToMigrate?: string[]; + enumNamesToKeep?: string[]; + modelScope?: string; + contextMappingOutput: ContextMap | null; +} { + const fullOptions: { + outputPath: string; + documentPath: string; + defaultContextTypePath?: string | null; + contextName?: string; + legacyCompat?: boolean; + legacyEnumsCompatibility?: boolean; + legacyNoModelsForObjects?: boolean; + useStringUnionsInsteadOfEnums?: boolean; + enumNamesToMigrate?: string[]; + enumNamesToKeep?: string[]; + contextSubTypeNameTemplate?: string; + contextSubTypePathTemplate?: string; + } = { + outputPath: "__generated__", + documentPath: "./typedef.graphql", + ...options, + }; + const document = parse(doc); + const { files, contextMappingOutput } = generateTS(document, fullOptions); + + function getFileByFileName(fileName: string) { + return files.find((file) => file.fileName === fileName); + } + + const { models, resolvers, enums, inputs, legacyTypes, legacyResolvers } = { + models: getFileByFileName("models.interface.ts") as ts.SourceFile, + inputs: getFileByFileName("inputs.interface.ts"), + enums: getFileByFileName("enums.interface.ts"), + resolvers: getFileByFileName("resolvers.interface.ts") as ts.SourceFile, + legacyTypes: getFileByFileName("legacy-types.interface.ts"), + legacyResolvers: getFileByFileName("legacy-resolvers.interface.ts"), + }; + + const printer = ts.createPrinter(); + + return { + enums: enums && printer.printFile(enums), + inputs: inputs && printer.printFile(inputs), + models: printer.printFile(models), + resolvers: printer.printFile(resolvers), + legacyTypes: legacyTypes && printer.printFile(legacyTypes), + legacyResolvers: legacyResolvers && printer.printFile(legacyResolvers), + contextMappingOutput, + }; +} diff --git a/packages/ts-codegen/src/__tests__/index.test.ts b/packages/ts-codegen/src/__tests__/index.test.ts index 9c5794e7b..f92107b33 100644 --- a/packages/ts-codegen/src/__tests__/index.test.ts +++ b/packages/ts-codegen/src/__tests__/index.test.ts @@ -1639,7 +1639,7 @@ describe(generateTS, () => { }); }); - it("generateTS without ContextName and ContextImport", () => { + it("generateTS without ContextName and defaultContextTypePath", () => { const { models, resolvers, enums, inputs } = runGenerateTest(graphql` interface Node { id: ID! @@ -2299,8 +2299,8 @@ function runGenerateTest( options: { outputPath?: string; documentPath?: string; - contextImport?: string; - contextName?: string; + defaultContextTypePath?: string; + contextTypeName?: string; legacyCompat?: boolean; enumsImport?: string; legacyNoModelsForObjects?: boolean; @@ -2327,8 +2327,8 @@ function runGenerateTest( const fullOptions: { outputPath: string; documentPath: string; - contextImport?: string | null; - contextName?: string; + defaultContextTypePath?: string | null; + contextTypeName?: string; legacyCompat?: boolean; legacyEnumsCompatibility?: boolean; legacyNoModelsForObjects?: boolean; diff --git a/packages/ts-codegen/src/codegen.ts b/packages/ts-codegen/src/codegen.ts index 281b67095..43c061fe7 100644 --- a/packages/ts-codegen/src/codegen.ts +++ b/packages/ts-codegen/src/codegen.ts @@ -1,6 +1,6 @@ import ts from "typescript"; import { DocumentNode } from "graphql"; -import { extractContext } from "./context/index"; +import { ContextMap, extractContext } from "./context/index"; import { generateResolvers } from "./resolvers"; import { generateModels } from "./models"; import { generateLegacyTypes } from "./legacyTypes"; @@ -13,8 +13,8 @@ export function generateTS( { outputPath, documentPath, - contextImport, - contextName, + contextTypePath, + contextTypeName, enumsImport, legacyCompat, useStringUnionsInsteadOfEnums, @@ -23,11 +23,15 @@ export function generateTS( generateOnlyEnums, enumNamesToMigrate, enumNamesToKeep, + contextSubTypeNameTemplate, + contextSubTypePathTemplate, + defaultContextSubTypePath, + defaultContextSubTypeName, }: { outputPath: string; documentPath: string; - contextImport?: string | null; - contextName?: string; + contextTypePath?: string | null; + contextTypeName?: string; enumsImport?: string | null; legacyCompat?: boolean; useStringUnionsInsteadOfEnums?: boolean; @@ -36,16 +40,21 @@ export function generateTS( generateOnlyEnums?: boolean; enumNamesToMigrate?: string[]; enumNamesToKeep?: string[]; + contextSubTypeNameTemplate?: string; + contextSubTypePathTemplate?: string; + defaultContextSubTypePath?: string; + defaultContextSubTypeName?: string; }, ): { files: ts.SourceFile[]; + contextMappingOutput: ContextMap | null; } { try { const context = extractContext( { context: { - name: contextName, - from: contextImport || null, + name: contextTypeName, + from: contextTypePath || null, }, legacyCompat, useStringUnionsInsteadOfEnums, @@ -54,6 +63,10 @@ export function generateTS( modelScope, enumNamesToMigrate, enumNamesToKeep, + contextSubTypeNameTemplate, + contextSubTypePathTemplate, + defaultContextSubTypePath, + defaultContextSubTypeName, }, document, outputPath, @@ -76,7 +89,10 @@ export function generateTS( result.push(generateLegacyResolvers(context)); } } - return { files: result }; + return { + files: result, + contextMappingOutput: context.getContextMap(), + }; } catch (e) { console.error(e); throw e; diff --git a/packages/ts-codegen/src/context/index.ts b/packages/ts-codegen/src/context/index.ts index c0fc75971..e58e82f1b 100644 --- a/packages/ts-codegen/src/context/index.ts +++ b/packages/ts-codegen/src/context/index.ts @@ -25,6 +25,7 @@ import ts, { import { DefinitionImport, DefinitionModel } from "../types"; import { createImportDeclaration } from "./utilities"; import { + camelCase, createListType, createNonNullableType, createNullableType, @@ -51,6 +52,10 @@ export type TsCodegenContextOptions = { }; legacyCompat: boolean; legacyNoModelsForObjects: boolean; + contextSubTypePathTemplate?: string; + contextSubTypeNameTemplate?: string; + defaultContextSubTypePath?: string; + defaultContextSubTypeName?: string; useStringUnionsInsteadOfEnums: boolean; enumNamesToMigrate: string[] | null; enumNamesToKeep: string[] | null; @@ -95,8 +100,16 @@ const TsCodegenContextDefault: TsCodegenContextOptions = { type ModelNameAndImport = { modelName: string; imp: DefinitionImport }; +export type ContextMap = { + [key: string]: ContextMapTypeItem; +}; + +export type ContextMapTypeItem = { __context?: string[] } & { + [key: string]: string[]; +}; export class TsCodegenContext { private allTypes: Array; + private typeContextMap: ContextMap; private typeNameToType: Map; private usedEntitiesInModels: Set; private usedEntitiesInResolvers: Set; @@ -108,6 +121,12 @@ export class TsCodegenContext { >; private typeNameToModels: Map; private legacyInterfaces: Set; + context?: { name: string; from: string }; + contextDefaultSubTypeTemplate?: { + nameTemplate: string; + pathTemplate: string; + }; + contextDefaultSubTypeContext?: { name: string; from: string }; hasUsedModelInInputs: boolean; hasUsedEnumsInModels: boolean; hasEnums: boolean; @@ -115,6 +134,7 @@ export class TsCodegenContext { constructor(private options: TsCodegenContextOptions) { this.allTypes = []; + this.typeContextMap = {}; this.typeNameToType = new Map(); this.usedEntitiesInModels = new Set(); this.usedEntitiesInResolvers = new Set(); @@ -128,6 +148,178 @@ export class TsCodegenContext { this.hasInputs = false; this.hasEnums = Boolean(options.enumsImport); this.hasUsedEnumsInModels = false; + + if ( + options.contextSubTypeNameTemplate && + options.contextSubTypePathTemplate + ) { + this.contextDefaultSubTypeTemplate = { + nameTemplate: options.contextSubTypeNameTemplate, + pathTemplate: options.contextSubTypePathTemplate, + }; + } + + if ( + options.defaultContextSubTypeName && + options.defaultContextSubTypePath + ) { + this.contextDefaultSubTypeContext = { + name: options.defaultContextSubTypeName, + from: options.defaultContextSubTypePath, + }; + } + + if (options.context.from && options.context.name) { + this.context = { + name: options.context.name, + from: options.context.from, + }; + } + } + + public getContextTypes( + contextRootType: T & { + __context?: string[]; + }, + ): string[] | null { + if (contextRootType) { + if (contextRootType.__context) { + return contextRootType.__context; + } + } + return null; + } + + public replaceTemplateWithContextName( + template: string, + contextName: string, + camelCased = true, + ) { + return template.replace( + "${contextName}", + camelCased ? camelCase(contextName, { pascalCase: true }) : contextName, + ); + } + + public getContextTemplate() { + return this.contextDefaultSubTypeTemplate || null; + } + + public getContextTypeNode(typeNames?: string[] | null) { + const contextDefaultSubTypeTemplate = this.contextDefaultSubTypeTemplate; + + if (!typeNames || !typeNames.length || !contextDefaultSubTypeTemplate) { + return this.getContextType().toTypeReference(); + } else if ( + (typeNames.length === 1 && this.contextDefaultSubTypeContext) || + typeNames.length > 1 + ) { + const typeNameWithNamespace = typeNames.map((typeName) => { + return this.replaceTemplateWithContextName( + contextDefaultSubTypeTemplate.nameTemplate, + typeName, + ); + }); + + return factory.createIntersectionTypeNode( + (this.contextDefaultSubTypeContext + ? [this.contextDefaultSubTypeContext.name, ...typeNameWithNamespace] + : typeNameWithNamespace + ).map((type: string) => { + return factory.createTypeReferenceNode( + factory.createIdentifier(type), + undefined, + ); + }), + ); + } else { + return new TypeLocation( + null, + this.replaceTemplateWithContextName( + contextDefaultSubTypeTemplate.nameTemplate, + typeNames[0], + ), + ).toTypeReference(); + } + } + + private isNonArrayNode( + node: ASTNode | ReadonlyArray, + ): node is ASTNode { + return !Array.isArray(node); + } + + public initContextMap( + ancestors: ReadonlyArray>, + values: string[], + ) { + if (ancestors.length < 2) { + throw new Error("Invalid document provided"); + } + + const node = ancestors[ancestors.length - 1]; + const nonArrayNode = this.isNonArrayNode(node) ? node : null; + + if (nonArrayNode) { + if ( + nonArrayNode?.kind === "ObjectTypeDefinition" || + nonArrayNode?.kind === "InterfaceTypeDefinition" || + nonArrayNode?.kind === "UnionTypeDefinition" + ) { + if (this.typeContextMap[nonArrayNode.name.value]?.__context) { + throw new Error("Type already visited"); + } + + const typeName = nonArrayNode.name.value; + if (!this.typeContextMap[typeName]) { + this.typeContextMap[typeName] = {}; + } + + this.typeContextMap[typeName].__context = values; + } else if (nonArrayNode?.kind === "FieldDefinition") { + const node = ancestors[ancestors.length - 3]; + const typeName = + this.isNonArrayNode(node) && + (node.kind === "ObjectTypeDefinition" || + node.kind === "ObjectTypeExtension") + ? node.name.value + : null; + + if (typeName) { + if (!this.typeContextMap[typeName]) { + this.typeContextMap[typeName] = {}; + } + + this.typeContextMap[typeName][nonArrayNode.name.value] = values; + } + } + } + } + + getSubTypeNamesFromTemplate( + subTypes: string[], + nameTemplate: string, + pathTemplate: string, + ) { + return subTypes.reduce>( + (acc: Record, importName: string) => { + const importPath = this.replaceTemplateWithContextName( + pathTemplate, + importName, + false, + ); + if (importPath) { + if (!acc[importPath]) { + acc[importPath] = []; + } + acc[importPath].push( + this.replaceTemplateWithContextName(nameTemplate, importName), + ); + } + return acc; + }, + {}, + ); } isLegacyCompatMode(): boolean { @@ -142,6 +334,10 @@ export class TsCodegenContext { return this.options.enumsImport; } + getContextMap() { + return this.typeContextMap; + } + addType(type: Type): void { this.allTypes.push(type); this.typeNameToType.set(type.name, type); @@ -188,6 +384,20 @@ export class TsCodegenContext { } } + getTypeFromTypeNode(node: TypeNode): string { + if (typeof node === "string") { + return node; + } + + if (node.kind === Kind.NON_NULL_TYPE) { + return this.getTypeFromTypeNode(node.type); + } else if (node.kind === Kind.LIST_TYPE) { + return this.getTypeFromTypeNode(node.type); + } else { + return node.name.value; + } + } + isUseStringUnionsInsteadOfEnumsEnabled(): boolean { return Boolean(this.options.useStringUnionsInsteadOfEnums); } @@ -525,7 +735,9 @@ export function extractContext( ...TsCodegenContextDefault, ...options, }; + const context = new TsCodegenContext(fullOptions); + const { contextSubTypeNameTemplate, contextSubTypePathTemplate } = options; visit(document, { Directive: { @@ -556,6 +768,28 @@ export function extractContext( } const typeName = (typeDef as InterfaceTypeDefinitionNode).name.value; context.addLegacyInterface(typeName); + } else if ( + node.name.value === "context" && + contextSubTypeNameTemplate && + contextSubTypePathTemplate + ) { + if ( + node.arguments?.length !== 1 || + node.arguments[0].name.value !== "stateMachines" || + node.arguments[0].value.kind !== "ListValue" + ) { + throw new Error("Invalid context use"); + } + const directiveValues = node.arguments[0].value.values.map((item) => { + if (item.kind !== "StringValue") { + throw new Error("Invalid context use"); + } + return item.value; + }); + + if (directiveValues.length) { + context.initContextMap(ancestors, directiveValues); + } } }, }, diff --git a/packages/ts-codegen/src/context/utilities.ts b/packages/ts-codegen/src/context/utilities.ts index a2f9d67ca..4a9805bb9 100644 --- a/packages/ts-codegen/src/context/utilities.ts +++ b/packages/ts-codegen/src/context/utilities.ts @@ -1,6 +1,36 @@ -import { factory } from "typescript"; +import ts, { factory } from "typescript"; import path from "path"; +export function getImportIdentifierForTypenames( + importNames: string[], + importPath: string, + contextImportNames: Set, +) { + return factory.createImportDeclaration( + undefined, + factory.createImportClause( + true, + undefined, + factory.createNamedImports( + importNames + .map((importName: string) => { + if (contextImportNames.has(importName)) { + return; + } + contextImportNames.add(importName); + return factory.createImportSpecifier( + false, + undefined, + factory.createIdentifier(importName), + ); + }) + .filter(Boolean) as ts.ImportSpecifier[], + ), + ), + factory.createStringLiteral(importPath), + ); +} + export function createImportDeclaration( importNames: string[], from: string, diff --git a/packages/ts-codegen/src/resolvers.ts b/packages/ts-codegen/src/resolvers.ts index e58d8737d..c7e9f4248 100644 --- a/packages/ts-codegen/src/resolvers.ts +++ b/packages/ts-codegen/src/resolvers.ts @@ -13,6 +13,10 @@ import { createUnionResolveType, createInterfaceResolveType, } from "./utilities"; +import { + createImportDeclaration, + getImportIdentifierForTypenames, +} from "./context/utilities"; export function generateResolvers(context: TsCodegenContext): ts.SourceFile { const statements: ts.Statement[] = []; @@ -29,6 +33,79 @@ export function generateResolvers(context: TsCodegenContext): ts.SourceFile { ), ); + const contextTemplate = context.getContextTemplate(); + + if (Object.keys(context.getContextMap()).length && contextTemplate) { + if ( + context.contextDefaultSubTypeContext?.from && + context.contextDefaultSubTypeContext?.name + ) { + statements.push( + createImportDeclaration( + [context.contextDefaultSubTypeContext.name], + context.contextDefaultSubTypeContext.from, + ), + ); + } + + const contextImportNames: Set = new Set(); + for (const [, root] of Object.entries(context.getContextMap())) { + const rootValue: string[] | undefined = root.__context; + if (rootValue) { + if ( + rootValue.every((importName: string) => + contextImportNames.has(importName), + ) + ) { + continue; + } + const imports = context.getSubTypeNamesFromTemplate( + rootValue, + contextTemplate.nameTemplate, + contextTemplate.pathTemplate, + ); + + for (const [importPath, importNames] of Object.entries(imports)) { + statements.push( + getImportIdentifierForTypenames( + importNames, + importPath, + contextImportNames, + ), + ); + } + } + for (const [key, value] of Object.entries(root)) { + if (key.startsWith("__")) { + continue; + } + if ( + value.every((importName: string) => + contextImportNames.has(importName), + ) + ) { + continue; + } + + const imports = context.getSubTypeNamesFromTemplate( + value, + contextTemplate.nameTemplate, + contextTemplate.pathTemplate, + ); + + for (const [importPath, importNames] of Object.entries(imports)) { + statements.push( + getImportIdentifierForTypenames( + importNames, + importPath, + contextImportNames, + ), + ); + } + } + } + } + if (context.hasInputs) { statements.push( factory.createImportDeclaration( @@ -128,13 +205,16 @@ function createObjectTypeResolvers( ); } +function isRootOperationType(type: string): boolean { + return ["Query", "Mutation", "Subscription"].includes(type); +} function createResolverField( context: TsCodegenContext, type: Type, field: Field, ): ts.TypeAliasDeclaration { let modelIdentifier; - if (["Query", "Mutation", "Subscription"].includes(type.name)) { + if (isRootOperationType(type.name)) { modelIdentifier = factory.createKeywordTypeNode( ts.SyntaxKind.UnknownKeyword, ); @@ -144,6 +224,20 @@ function createResolverField( .toTypeReference(); } + const contextRootType = + context.getContextMap()[type.name] || + (!isRootOperationType(type.name) && + context.getContextMap()[context.getTypeFromTypeNode(field.type)]); + + let contextTypes; + if (contextRootType) { + if (contextRootType[field.name]) { + contextTypes = contextRootType[field.name]; + } else if (contextRootType.__context) { + contextTypes = contextRootType.__context; + } + } + const resolverParametersDefinitions = { parent: { name: "model", @@ -164,7 +258,7 @@ function createResolverField( }, context: { name: "context", - type: context.getContextType().toTypeReference(), + type: context.getContextTypeNode(contextTypes), }, resolveInfo: { name: "info", @@ -179,6 +273,7 @@ function createResolverField( field.name, ); } + return factory.createTypeAliasDeclaration( [factory.createModifier(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier(toValidFieldName(field.name)), @@ -218,6 +313,11 @@ function createUnionTypeResolvers( ), ], ); + + const contextRootType = context.getContextMap()[type.name]; + + const contextTypes = context.getContextTypes(contextRootType); + return factory.createModuleDeclaration( [ factory.createModifier(ts.SyntaxKind.ExportKeyword), @@ -230,7 +330,11 @@ function createUnionTypeResolvers( [factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier("__resolveType"), undefined, - createUnionResolveType(context, type), + createUnionResolveType( + context, + type, + context.getContextTypeNode(contextTypes), + ), ), ]), ts.NodeFlags.Namespace, @@ -241,6 +345,9 @@ function createInterfaceTypeResolvers( context: TsCodegenContext, type: InterfaceType, ): ts.ModuleDeclaration { + const contextRootType = context.getContextMap()[type.name]; + const contextTypes = context.getContextTypes(contextRootType); + const resolversObject = factory.createInterfaceDeclaration( [factory.createModifier(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier("Resolvers"), @@ -268,7 +375,10 @@ function createInterfaceTypeResolvers( [factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier("__resolveType"), undefined, - createInterfaceResolveType(context), + createInterfaceResolveType( + context, + context.getContextTypeNode(contextTypes), + ), ), ]), ts.NodeFlags.Namespace, diff --git a/packages/ts-codegen/src/utilities.ts b/packages/ts-codegen/src/utilities.ts index 4591fed3d..c0aac7b03 100644 --- a/packages/ts-codegen/src/utilities.ts +++ b/packages/ts-codegen/src/utilities.ts @@ -23,13 +23,16 @@ type ResolverParameterDefinition = { name: string; type: T }; type ResolverParametersDefinitions = { parent: ResolverParameterDefinition; args?: ResolverParameterDefinition; - context: ResolverParameterDefinition; + context: ResolverParameterDefinition< + ts.TypeReferenceNode | ts.IntersectionTypeNode + >; resolveInfo: ResolverParameterDefinition; }; export function createUnionResolveType( context: TsCodegenContext, type: UnionType, + contextType: ts.TypeReferenceNode | ts.IntersectionTypeNode, ): ts.FunctionTypeNode { return factory.createFunctionTypeNode( undefined, @@ -44,7 +47,7 @@ export function createUnionResolveType( }, context: { name: "context", - type: context.getContextType().toTypeReference(), + type: contextType, }, resolveInfo: { name: "info", @@ -68,6 +71,7 @@ export function createUnionResolveType( export function createInterfaceResolveType( context: TsCodegenContext, + contextType: ts.TypeReferenceNode | ts.IntersectionTypeNode, ): ts.FunctionTypeNode { return factory.createFunctionTypeNode( undefined, @@ -78,7 +82,7 @@ export function createInterfaceResolveType( }, context: { name: "context", - type: context.getContextType().toTypeReference(), + type: contextType, }, resolveInfo: { name: "info", diff --git a/packages/webpack-loader/CHANGELOG.json b/packages/webpack-loader/CHANGELOG.json index de3331ae3..bc0494d85 100644 --- a/packages/webpack-loader/CHANGELOG.json +++ b/packages/webpack-loader/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@graphitation/webpack-loader", "entries": [ + { + "date": "Wed, 30 Oct 2024 09:41:18 GMT", + "version": "1.0.13", + "tag": "@graphitation/webpack-loader_v1.0.13", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@graphitation/webpack-loader", + "comment": "Bump @graphitation/supermassive to v3.6.3", + "commit": "not available" + } + ] + } + }, { "date": "Thu, 24 Oct 2024 13:15:11 GMT", "version": "1.0.12", diff --git a/packages/webpack-loader/CHANGELOG.md b/packages/webpack-loader/CHANGELOG.md index 6a5a2d666..ce2be76cc 100644 --- a/packages/webpack-loader/CHANGELOG.md +++ b/packages/webpack-loader/CHANGELOG.md @@ -1,9 +1,17 @@ # Change Log - @graphitation/webpack-loader - + +## 1.0.13 + +Wed, 30 Oct 2024 09:41:18 GMT + +### Patches + +- Bump @graphitation/supermassive to v3.6.3 + ## 1.0.12 Thu, 24 Oct 2024 13:15:11 GMT diff --git a/packages/webpack-loader/package.json b/packages/webpack-loader/package.json index c397c80c6..378b3f7b5 100644 --- a/packages/webpack-loader/package.json +++ b/packages/webpack-loader/package.json @@ -1,7 +1,7 @@ { "name": "@graphitation/webpack-loader", "license": "MIT", - "version": "1.0.12", + "version": "1.0.13", "description": "A fork of @graphql-tools/webpack-loader with supermassive SDL encoding format support", "repository": { "type": "git", @@ -17,7 +17,7 @@ }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", - "@graphitation/supermassive": "^3.6.2" + "@graphitation/supermassive": "^3.6.3" }, "dependencies": { "@graphql-tools/optimize": "^1.1.1",