Graphql codegen resolvers models plugin (#208)

* graphql-codegen-resolvers-models added
This commit is contained in:
vejrj 2022-08-16 10:22:01 +02:00 коммит произвёл GitHub
Родитель 38d61cfead
Коммит d8827eb215
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 437 добавлений и 30 удалений

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "graphql-codegen-resolvers-models added",
"packageName": "@graphitation/graphql-codegen-resolvers-models",
"email": "jakubvejr@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,4 @@
{
"extends": ["../../.eslintrc.json"],
"root": true
}

Просмотреть файл

@ -0,0 +1,5 @@
# Custom GraphQL codegen plugins
## graphql-codegen-resolvers-models
It allows us to intersect public types with models and also omit fields if needed

Просмотреть файл

@ -0,0 +1,45 @@
{
"name": "@graphitation/graphql-codegen-resolvers-models",
"license": "MIT",
"version": "1.0.0",
"main": "./src/index.ts",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/graphitation.git",
"directory": "packages/graphql-codegen-resolvers-models"
},
"scripts": {
"build": "monorepo-scripts build",
"lint": "monorepo-scripts lint",
"types": "monorepo-scripts types",
"test": "monorepo-scripts test",
"just": "monorepo-scripts"
},
"devDependencies": {
"@graphql-codegen/plugin-helpers": "^1.18.2",
"@graphql-codegen/visitor-plugin-common": "^1.17.20",
"@types/jest": "^26.0.22",
"monorepo-scripts": "*"
},
"peerDependencies": {
"@graphql-codegen/plugin-helpers": ">= 1.18.0 < 2",
"@graphql-codegen/visitor-plugin-common": ">= ^1.17.0 < 2",
"typescript": "^4.4.3 <4.5.0"
},
"dependencies": {
"typescript": "^4.4.3 <4.5.0"
},
"sideEffects": false,
"access": "public",
"publishConfig": {
"main": "./lib/index",
"types": "./lib/index.d.ts",
"module": "./lib/index.mjs",
"exports": {
".": {
"import": "./lib/index.mjs",
"require": "./lib/index.js"
}
}
}
}

Просмотреть файл

@ -0,0 +1,16 @@
import { RawConfig } from "@graphql-codegen/visitor-plugin-common";
export type MapperConfigValue = {
extend?: boolean;
exclude?: string[];
};
export interface ResolversModelsPluginConfig extends RawConfig {
mappers?: { [typeName: string]: string };
mapperTypeSuffix?: string;
mappersConfig?: {
[typeName: string]: MapperConfigValue;
};
federation?: boolean;
modelIntersectionSuffix?: string;
namespacedImportName?: string;
}

Просмотреть файл

@ -0,0 +1,165 @@
import { printSchemaWithDirectives } from "@graphql-tools/utils";
import ts, { factory, TypeAliasDeclaration } from "typescript";
import { ParsedMapper } from "@graphql-codegen/visitor-plugin-common";
import {
Types,
PluginFunction,
addFederationReferencesToSchema,
} from "@graphql-codegen/plugin-helpers";
import { parse, visit, GraphQLSchema, printSchema } from "graphql";
import { ResolversModelsVisitor } from "./visitor";
import { ResolversModelsPluginConfig, MapperConfigValue } from "./config";
export const plugin: PluginFunction<
ResolversModelsPluginConfig,
Types.ComplexPluginOutput
> = (
schema: GraphQLSchema,
documents: Types.DocumentFile[],
config: ResolversModelsPluginConfig,
) => {
const transformedSchema = config.federation
? addFederationReferencesToSchema(schema)
: schema;
if (!config.modelIntersectionSuffix || !config.mappers) {
return { content: "" };
}
const visitor = new ResolversModelsVisitor(config, transformedSchema);
const printedSchema = config.federation
? printSchemaWithDirectives(transformedSchema)
: printSchema(transformedSchema);
const astNode = parse(printedSchema);
visit(astNode, { leave: visitor } as any);
const content = Object.entries(visitor.getValidMappers()).reduce(
(acc, [typeName, model]) => {
acc.push(
...getModelAST(
typeName,
model,
config.modelIntersectionSuffix as string,
config.namespacedImportName || "",
config?.mappersConfig?.[typeName],
),
);
return acc;
},
[] as TypeAliasDeclaration[],
);
const tsContents = factory.createSourceFile(
content,
factory.createToken(ts.SyntaxKind.EndOfFileToken),
0,
);
const printer = ts.createPrinter();
return {
content: printer.printNode(ts.EmitHint.SourceFile, tsContents, tsContents),
};
};
function getOmittedFields(typeName: string, omitFields?: string[]) {
if (!omitFields || !omitFields.length) {
return;
}
return factory.createTypeAliasDeclaration(
undefined,
undefined,
factory.createIdentifier(`${typeName}ModelOmitFields`),
undefined,
factory.createUnionTypeNode(
omitFields.map((field) =>
factory.createLiteralTypeNode(factory.createStringLiteral(field)),
),
),
);
}
function getModelAST(
typeName: string,
mapper: ParsedMapper,
modelIntersectionSuffix: string,
namespacedImportName: string,
mapperConfig?: MapperConfigValue,
) {
if (!mapperConfig || !mapperConfig.extend) {
return [
factory.createTypeAliasDeclaration(
undefined,
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createIdentifier(mapper.type),
undefined,
factory.createTypeReferenceNode(
factory.createIdentifier(`${mapper.type}${modelIntersectionSuffix}`),
undefined,
),
),
];
}
const omittedFields = getOmittedFields(typeName, mapperConfig.exclude);
if (!omittedFields) {
return [
factory.createTypeAliasDeclaration(
undefined,
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createIdentifier(mapper.type),
undefined,
factory.createIntersectionTypeNode([
factory.createTypeReferenceNode(
factory.createIdentifier(
`${
namespacedImportName ? namespacedImportName + "." : ""
}${typeName}`,
),
undefined,
),
factory.createTypeReferenceNode(
factory.createIdentifier(
`${mapper.type}${modelIntersectionSuffix}`,
),
undefined,
),
]),
),
];
}
return [
omittedFields,
factory.createTypeAliasDeclaration(
undefined,
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createIdentifier(mapper.type),
undefined,
factory.createIntersectionTypeNode([
factory.createTypeReferenceNode(factory.createIdentifier("Omit"), [
factory.createTypeReferenceNode(
factory.createIdentifier(
`${
namespacedImportName ? namespacedImportName + "." : ""
}${typeName}`,
),
undefined,
),
factory.createTypeReferenceNode(
factory.createIdentifier(`${typeName}ModelOmitFields`),
undefined,
),
]),
factory.createTypeReferenceNode(
factory.createIdentifier(`${mapper.type}${modelIntersectionSuffix}`),
undefined,
),
]),
),
];
}
export { ResolversModelsVisitor };

Просмотреть файл

@ -0,0 +1,122 @@
import { buildSchema } from "graphql";
import { plugin } from "../src";
const testSchema = buildSchema(/* GraphQL */ `
schema {
query: Query
}
type Query {
role: [ProjectRoleDetail!]!
}
type ProjectRoleDetail {
code: String!
name: String!
email: String!
}
`);
describe("TypeScript Resolvers Plugin", () => {
it("generates models with omitted fields", async () => {
const config = {
modelIntersectionSuffix: "Template",
mappersConfig: {
ProjectRoleDetail: {
extend: true,
exclude: ["name", "email"],
},
},
mappers: {
ProjectRoleDetail: "../entities#ProjectRole",
},
};
const output = await plugin(testSchema, [], config, {
outputFile: "graphql.ts",
});
expect(output.content).toMatchInlineSnapshot(`
"type ProjectRoleDetailModelOmitFields = \\"name\\" | \\"email\\";
export type ProjectRole = Omit<ProjectRoleDetail, ProjectRoleDetailModelOmitFields> & ProjectRoleTemplate;
"
`);
});
it("generates the models without omitted fields", async () => {
const config = {
modelIntersectionSuffix: "Template",
mappersConfig: {
ProjectRoleDetail: {
extend: true,
},
},
mappers: {
ProjectRoleDetail: "../entities#ProjectRole",
},
};
const output = await plugin(testSchema, [], config, {
outputFile: "graphql.ts",
});
expect(output.content).toMatchInlineSnapshot(`
"export type ProjectRole = ProjectRoleDetail & ProjectRoleTemplate;
"
`);
});
it("just assigns alias into the type, which is used in resolverTypes", async () => {
const config = {
modelIntersectionSuffix: "Template",
mappers: {
ProjectRoleDetail: "../entities#ProjectRole",
},
};
const output = await plugin(testSchema, [], config, {
outputFile: "graphql.ts",
});
expect(output.content).toMatchInlineSnapshot(`
"export type ProjectRole = ProjectRoleTemplate;
"
`);
});
it("doesn't generate anything", async () => {
const baseConfig = {
mappersConfig: {
ProjectRoleDetail: {
extend: true,
},
},
};
expect(
((await plugin(
testSchema,
[],
{
...baseConfig,
mappers: {
ProjectRoleDetail: "../entities#ProjectRole",
},
},
{
outputFile: "graphql.ts",
},
)) as any).content,
).toEqual("");
expect(
((await plugin(
testSchema,
[],
{
...baseConfig,
modelIntersectionSuffix: "Template",
},
{
outputFile: "graphql.ts",
},
)) as any).content,
).toEqual("");
});
});

Просмотреть файл

@ -0,0 +1,49 @@
import { ResolversModelsPluginConfig } from "./config";
import { GraphQLSchema } from "graphql";
import { ApolloFederation } from "@graphql-codegen/plugin-helpers";
import autoBind from "auto-bind";
import {
ParsedResolversConfig,
BaseVisitor,
getConfigValue,
transformMappers,
ParsedMapper,
} from "@graphql-codegen/visitor-plugin-common";
export class ResolversModelsVisitor<
TRawConfig extends ResolversModelsPluginConfig = ResolversModelsPluginConfig,
TPluginConfig extends ParsedResolversConfig = ParsedResolversConfig
> extends BaseVisitor<TRawConfig, TPluginConfig> {
protected _federation: ApolloFederation;
constructor(rawConfig: TRawConfig, private schema: GraphQLSchema) {
super(rawConfig, {
federation: getConfigValue(rawConfig.federation, false),
mappers: transformMappers(
rawConfig.mappers || {},
rawConfig.mapperTypeSuffix,
),
} as TPluginConfig);
autoBind(this);
this._federation = new ApolloFederation({
enabled: this.config.federation,
schema: this.schema,
});
}
public getValidMappers() {
const allSchemaTypes = this.schema.getTypeMap();
const typeNames = this._federation.filterTypeNames(
Object.keys(allSchemaTypes),
);
return typeNames.reduce((acc, typeName) => {
const isMapped = this.config.mappers[typeName];
if (isMapped) {
acc[typeName] = this.config.mappers[typeName];
}
return acc;
}, {} as { [typename: string]: ParsedMapper });
}
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": ".tsbuildinfo",
"rootDir": "src",
"outDir": "lib"
},
"include": ["src"],
"references": []
}

Просмотреть файл

@ -5987,22 +5987,7 @@ git-url-parse@^11.1.2:
dependencies:
git-up "^4.0.0"
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
integrity sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==
dependencies:
is-glob "^3.1.0"
path-dirname "^1.0.0"
glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
glob-parent@^6.0.1:
glob-parent@^3.1.0, glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@^6.0.1, glob-parent@^6.0.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
@ -6733,7 +6718,7 @@ is-extendable@^1.0.1:
dependencies:
is-plain-object "^2.0.4"
is-extglob@^2.1.0, is-extglob@^2.1.1:
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
@ -6767,13 +6752,6 @@ is-glob@4.0.3, is-glob@^4.0.1, is-glob@^4.0.3:
dependencies:
is-extglob "^2.1.1"
is-glob@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
integrity sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==
dependencies:
is-extglob "^2.1.0"
is-glob@^4.0.0, is-glob@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
@ -8239,7 +8217,7 @@ minimatch@^3.1.1, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.5:
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
@ -8401,6 +8379,11 @@ node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5, node-
dependencies:
whatwg-url "^5.0.0"
node-forge@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@ -8863,11 +8846,6 @@ path-case@^3.0.4:
dot-case "^3.0.4"
tslib "^2.0.3"
path-dirname@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
integrity sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@ -10377,6 +10355,11 @@ trim-repeated@^1.0.0:
dependencies:
escape-string-regexp "^1.0.2"
trim@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim/-/trim-1.0.1.tgz#68e78f6178ccab9687a610752f4f5e5a7022ee8c"
integrity sha512-3JVP2YVqITUisXblCDq/Bi4P9457G/sdEamInkyvCsjbTcXLXIiG7XCb4kGMFWh6JGXesS3TKxOPtrncN/xe8w==
ts-expect@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ts-expect/-/ts-expect-1.3.0.tgz#3f8d3966e0e22b5e2bb88337eb99db6816a4c1cf"