Graphql codegen resolvers models plugin (#208)
* graphql-codegen-resolvers-models added
This commit is contained in:
Родитель
38d61cfead
Коммит
d8827eb215
|
@ -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": []
|
||||
}
|
43
yarn.lock
43
yarn.lock
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче