Skip to content

Commit

Permalink
fix: support namingConvention options
Browse files Browse the repository at this point in the history
  • Loading branch information
azu committed Jun 17, 2024
1 parent ab28135 commit d196ef0
Show file tree
Hide file tree
Showing 23 changed files with 11,473 additions and 14,025 deletions.
37 changes: 34 additions & 3 deletions e2e/api/graphql/api.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ Error interface
interface Error {
"Error message."
message: String!
"Localized error message."
localizedMessage: String!
}
type TextError implements Error {
"Error message."
Expand Down Expand Up @@ -93,10 +91,43 @@ type RideHistoryOutput {
"Error."
errors: [ErrorUnion!]!
}
"""
namingConvention test
- URL
- Create*
"""
type FooURLResource {
"Foo URL"
URL: String!
}
input FooURLInput {
"Foo URL"
URL: String!
}
type FooURLPayload {
"Foo URL"
URL: String!
"Errors"
errors: [CreateFooURLError!]!
}
union CreateFooURLError = CreateFooURLErrorDetail
type CreateFooURLErrorDetail implements Error {
code: CreateFooURLErrorCode!
message: String!
localizedMessage: String!
}
enum CreateFooURLErrorCode {
FAILED_TO_CREATE_FOO_URL
}

type Mutation {
"""
Create a ride history.
Create Ride History
"""
createRideHistory(input: RideHistoryInput!): RideHistoryOutput!

"""
Create Foo URL
"""
createFooURL(input: FooURLInput!): FooURLPayload!
}
55 changes: 52 additions & 3 deletions e2e/generated/typescript/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ export type Scalars = {
Float: { input: number; output: number; }
};

export type CreateFooUrlError = CreateFooUrlErrorDetail;

export enum CreateFooUrlErrorCode {
FailedToCreateFooUrl = 'FAILED_TO_CREATE_FOO_URL'
}

export type CreateFooUrlErrorDetail = Error & {
__typename: 'CreateFooURLErrorDetail';
code: CreateFooUrlErrorCode;
localizedMessage: Scalars['String']['output'];
message: Scalars['String']['output'];
};

/** Specified Error type for createRideHistory mutation. */
export type CreateRideHistoryErrorDetails = Error & {
__typename: 'CreateRideHistoryErrorDetails';
Expand Down Expand Up @@ -45,21 +58,50 @@ export enum DestinationType {

/** Error interface */
export type Error = {
/** Localized error message. */
localizedMessage: Scalars['String']['output'];
/** Error message. */
message: Scalars['String']['output'];
};

export type ErrorUnion = CreateRideHistoryErrorDetails | TextError;

export type FooUrlInput = {
/** Foo URL */
URL: Scalars['String']['input'];
};

export type FooUrlPayload = {
__typename: 'FooURLPayload';
/** Foo URL */
URL: Scalars['String']['output'];
/** Errors */
errors: Array<CreateFooUrlError>;
};

/**
* namingConvention test
* - URL
* - Create*
*/
export type FooUrlResource = {
__typename: 'FooURLResource';
/** Foo URL */
URL: Scalars['String']['output'];
};

export type Mutation = {
__typename: 'Mutation';
/** Create a ride history. */
/** Create Foo URL */
createFooURL: FooUrlPayload;
/** Create Ride History */
createRideHistory: RideHistoryOutput;
};


export type MutationCreateFooUrlArgs = {
input: FooUrlInput;
};


export type MutationCreateRideHistoryArgs = {
input: RideHistoryInput;
};
Expand Down Expand Up @@ -131,12 +173,19 @@ export type CreateRideHistoryMutation = { __typename: 'Mutation', createRideHist
export const ListDestinationCandidatesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListDestinationCandidates"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"text"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"destinationCandidates"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"text"},"value":{"kind":"Variable","name":{"kind":"Name","value":"text"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<ListDestinationCandidatesQuery, ListDestinationCandidatesQueryVariables>;
export const ListRideHistoriesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListRideHistories"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rideHistories"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"destination"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<ListRideHistoriesQuery, ListRideHistoriesQueryVariables>;
export const CreateRideHistoryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateRideHistory"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"desinationName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createRideHistory"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"desinationName"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TextError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"localizedMessage"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateRideHistoryMutation, CreateRideHistoryMutationVariables>;
export const isCreateFooUrlError = (field: { __typename?: string; }): field is CreateFooUrlError => {
if(field.__typename === undefined) return false;
return ["CreateFooUrlErrorDetail"].includes(field.__typename);
};
export const isCreateFooUrlErrorDetail = (field: { __typename?: string; }): field is CreateFooUrlErrorDetail => field.__typename === 'CreateFooUrlErrorDetail';
export const isCreateRideHistoryErrorDetails = (field: { __typename?: string; }): field is CreateRideHistoryErrorDetails => field.__typename === 'CreateRideHistoryErrorDetails';
export const isDestination = (field: { __typename?: string; }): field is Destination => field.__typename === 'Destination';
export const isErrorUnion = (field: { __typename?: string; }): field is ErrorUnion => {
if(field.__typename === undefined) return false;
return ["CreateRideHistoryErrorDetails","TextError"].includes(field.__typename);
};
export const isFooUrlPayload = (field: { __typename?: string; }): field is FooUrlPayload => field.__typename === 'FooUrlPayload';
export const isFooUrlResource = (field: { __typename?: string; }): field is FooUrlResource => field.__typename === 'FooUrlResource';
export const isMutation = (field: { __typename?: string; }): field is Mutation => field.__typename === 'Mutation';
export const isQuery = (field: { __typename?: string; }): field is Query => field.__typename === 'Query';
export const isRideHistory = (field: { __typename?: string; }): field is RideHistory => field.__typename === 'RideHistory';
Expand Down
2 changes: 1 addition & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@graphql-codegen/client-preset": "^4.2.4",
"@newmo/graphql-codegen-plugin-type-guards": "workspace:*",
"@newmo/graphql-codegen-plugin-typescript-react-apollo": "workspace:*",
"@newmo/graphql-fake-server": "^0.9.3",
"@newmo/graphql-fake-server": "^0.9.7",
"@playwright/test": "^1.44.1",
"@types/node": "^20",
"@types/react": "npm:types-react@rc",
Expand Down
7 changes: 7 additions & 0 deletions packages/@newmo/graphql-codegen-plugin-type-guards/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ Run codegen:

$ graphql-codegen --config graphql-codegen.ts

## Options

- `namingConvention` (optional): Naming convention for the generated types. Default is `change-case#pascalCase`.
- [Naming Convention](https://the-guild.dev/graphql/codegen/docs/config-reference/naming-convention)
- `typesPrefix` (optional): Prefix for the generated types.
- `typesSuffix` (optional): Suffix for the generated types.

## Example output

See [test/snapshots/typescript/graphql.ts](test/snapshots/typescript/graphql.ts) for example output.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"graphql": "^15.0.0 || ^16.0.0"
},
"dependencies": {
"@graphql-codegen/plugin-helpers": "^5.0.4"
"@graphql-codegen/plugin-helpers": "^5.0.4",
"@graphql-codegen/visitor-plugin-common": "^5.2.0"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
Expand Down
35 changes: 35 additions & 0 deletions packages/@newmo/graphql-codegen-plugin-type-guards/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { RawTypesConfig } from "@graphql-codegen/visitor-plugin-common";

export type RawPluginConfig = {
/**
* The path to the generated types file
* @example
* typeFile: "./graphql.ts"
**/
typesFile: string;
/**
* Naming convention for the generated types
* GraphQL Codegen Plugin Common Options
**/
namingConvention?: RawTypesConfig["namingConvention"];
/**
* Prefix for the generated types
* GraphQL Codegen Plugin Common Options
**/
typesPrefix?: RawTypesConfig["typesPrefix"];
/**
* Suffix for the generated types
* GraphQL Codegen Plugin Common Options
**/
typesSuffix?: RawTypesConfig["typesSuffix"];
};
export type PluginConfig = Required<RawPluginConfig>;

export function normalizeConfig(rawConfig: RawPluginConfig): PluginConfig {
return {
typesFile: rawConfig.typesFile,
typesPrefix: rawConfig.typesPrefix ?? "",
typesSuffix: rawConfig.typesSuffix ?? "",
namingConvention: rawConfig.namingConvention ?? ""
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { convertFactory } from "@graphql-codegen/visitor-plugin-common";
import type { ASTNode } from "graphql/index.js";
import type { PluginConfig } from "./config";

/**
* Convert the name of the node to the generated type name
* Client preset use this naming convention
* https://www.npmjs.com/package/@graphql-codegen/client-preset
* @param node
* @param config
*/
export function convertName(node: ASTNode | string, config: PluginConfig): string {
const convert = config.namingConvention
? convertFactory({ namingConvention: config.namingConvention })
: convertFactory({});
let convertedName = "";
convertedName += config.typesPrefix;
convertedName += convert(node);
convertedName += config.typesSuffix;
return convertedName;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PluginFunction, Types } from "@graphql-codegen/plugin-helpers";
import { GraphQLSchema, isIntrospectionType, isScalarType, isSpecifiedScalarType, isUnionType } from "graphql";
import { type GraphQLNamedType, isEnumType, isInputObjectType, isInterfaceType } from "graphql/type/definition";
import { normalizeConfig, PluginConfig, RawPluginConfig } from "./config";
import { convertName } from "./convertName";

const getUserDefinedTypes = (schema: GraphQLSchema) => {
const typeMap = schema.getTypeMap();
Expand Down Expand Up @@ -28,30 +30,31 @@ const getUserDefinedTypes = (schema: GraphQLSchema) => {
return { userDefinedTypes };
};

const getUnionTypes = (type: GraphQLNamedType) => {
const getUnionTypes = (type: GraphQLNamedType, config: PluginConfig) => {
// expand union type to A | B | C
if (isUnionType(type)) {
return type.getTypes().map((type) => type.name);
return type.getTypes().map((type) => convertName(type.name, config));
}
return [];
};

export const plugin: PluginFunction<Types.Config, Types.PluginOutput> = (schema, _, userDefinedConfig) => {
export const plugin: PluginFunction<RawPluginConfig, Types.PluginOutput> = (schema, _, userDefinedConfig) => {
const config = normalizeConfig(userDefinedConfig);
const { userDefinedTypes } = getUserDefinedTypes(schema);
return userDefinedTypes
.map((typeName) => {
// is union type
const typeInfo = schema.getType(typeName);
const convertedTypeName = convertName(typeName, config);
if (isUnionType(typeInfo)) {
const unionTypes = getUnionTypes(typeInfo);
const unionTypes = getUnionTypes(typeInfo, config);
return `\
export const is${typeName} = (field: { __typename?: string; }): field is ${typeName} => {
export const is${convertedTypeName} = (field: { __typename?: string; }): field is ${convertedTypeName} => {
if(field.__typename === undefined) return false;
return ${JSON.stringify(unionTypes)}.includes(field.__typename);
};`;
}

return `export const is${typeName} = (field: { __typename?: string; }): field is ${typeName} => field.__typename === '${typeName}';`;
return `export const is${convertedTypeName} = (field: { __typename?: string; }): field is ${convertedTypeName} => field.__typename === '${convertedTypeName}';`;
})
.join("\n");
};
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,43 @@ type RideHistoryOutput {
"Error."
errors: [ErrorUnion!]!
}
"""
namingConvention test
- URL
- Create*
"""
type FooURLResource {
"Foo URL"
URL: String!
}
input FooURLInput {
"Foo URL"
URL: String!
}
type FooURLPayload {
"Foo URL"
URL: String!
"Errors"
errors: [CreateFooURLError!]!
}
union CreateFooURLError = CreateFooURLErrorDetail
type CreateFooURLErrorDetail implements Error {
code: CreateFooURLErrorCode!
message: String!
localizedMessage: String!
}
enum CreateFooURLErrorCode {
FAILED_TO_CREATE_FOO_URL
}

type Mutation {
"""
Create a ride history.
Create Ride History
"""
createRideHistory(input: RideHistoryInput!): RideHistoryOutput!

"""
Create Foo URL
"""
createFooURL(input: FooURLInput!): FooURLPayload!
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ query ListDestinationCandidates($text: String!) {
destinationCandidates(text: $text) {
id
name
type
}
}

Expand All @@ -12,7 +11,6 @@ query ListRideHistories {
destination {
id
name
type
}
}
}
Expand All @@ -21,14 +19,18 @@ mutation CreateRideHistory($desinationName: String!) {
createRideHistory(input: { name: $desinationName }){
id
name
}
}

mutation CreateFooURL($input: FooURLInput!) {
createFooURL(input: $input) {
URL
errors {
... on TextError {
... on CreateFooURLErrorDetail{
message
code
localizedMessage
}
... on Error {
message
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"query ListDestinationCandidates($text: String!) {\n destinationCandidates(text: $text) {\n id\n name\n type\n }\n}\n\nquery ListRideHistories {\n rideHistories {\n id\n destination {\n id\n name\n type\n }\n }\n}\n\nmutation CreateRideHistory($desinationName: String!) {\n createRideHistory(input: {name: $desinationName}) {\n id\n name\n errors {\n ... on TextError {\n message\n localizedMessage\n }\n ... on Error {\n message\n }\n }\n }\n}": types.ListDestinationCandidatesDocument,
"query ListDestinationCandidates($text: String!) {\n destinationCandidates(text: $text) {\n id\n name\n }\n}\n\nquery ListRideHistories {\n rideHistories {\n id\n destination {\n id\n name\n }\n }\n}\n\nmutation CreateRideHistory($desinationName: String!) {\n createRideHistory(input: {name: $desinationName}) {\n id\n name\n }\n}\n\nmutation CreateFooURL($input: FooURLInput!) {\n createFooURL(input: $input) {\n URL\n errors {\n ... on CreateFooURLErrorDetail {\n message\n code\n localizedMessage\n }\n }\n }\n}": types.ListDestinationCandidatesDocument,
};

/**
Expand All @@ -33,7 +33,7 @@ export function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query ListDestinationCandidates($text: String!) {\n destinationCandidates(text: $text) {\n id\n name\n type\n }\n}\n\nquery ListRideHistories {\n rideHistories {\n id\n destination {\n id\n name\n type\n }\n }\n}\n\nmutation CreateRideHistory($desinationName: String!) {\n createRideHistory(input: {name: $desinationName}) {\n id\n name\n errors {\n ... on TextError {\n message\n localizedMessage\n }\n ... on Error {\n message\n }\n }\n }\n}"): (typeof documents)["query ListDestinationCandidates($text: String!) {\n destinationCandidates(text: $text) {\n id\n name\n type\n }\n}\n\nquery ListRideHistories {\n rideHistories {\n id\n destination {\n id\n name\n type\n }\n }\n}\n\nmutation CreateRideHistory($desinationName: String!) {\n createRideHistory(input: {name: $desinationName}) {\n id\n name\n errors {\n ... on TextError {\n message\n localizedMessage\n }\n ... on Error {\n message\n }\n }\n }\n}"];
export function graphql(source: "query ListDestinationCandidates($text: String!) {\n destinationCandidates(text: $text) {\n id\n name\n }\n}\n\nquery ListRideHistories {\n rideHistories {\n id\n destination {\n id\n name\n }\n }\n}\n\nmutation CreateRideHistory($desinationName: String!) {\n createRideHistory(input: {name: $desinationName}) {\n id\n name\n }\n}\n\nmutation CreateFooURL($input: FooURLInput!) {\n createFooURL(input: $input) {\n URL\n errors {\n ... on CreateFooURLErrorDetail {\n message\n code\n localizedMessage\n }\n }\n }\n}"): (typeof documents)["query ListDestinationCandidates($text: String!) {\n destinationCandidates(text: $text) {\n id\n name\n }\n}\n\nquery ListRideHistories {\n rideHistories {\n id\n destination {\n id\n name\n }\n }\n}\n\nmutation CreateRideHistory($desinationName: String!) {\n createRideHistory(input: {name: $desinationName}) {\n id\n name\n }\n}\n\nmutation CreateFooURL($input: FooURLInput!) {\n createFooURL(input: $input) {\n URL\n errors {\n ... on CreateFooURLErrorDetail {\n message\n code\n localizedMessage\n }\n }\n }\n}"];

export function graphql(source: string) {
return (documents as any)[source] ?? {};
Expand Down
Loading

0 comments on commit d196ef0

Please sign in to comment.