From b3ddf764383c4fd9fa3790f810a5e7a511dbeb51 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 17 Jan 2025 14:04:07 +0300 Subject: [PATCH] feat(grpc): `selectQueryOrMutationField` --- .changeset/large-oranges-draw.md | 46 +++++++ .../test/__snapshots__/handler.spec.ts.snap | 124 +++++++++++++++++- .../legacy/handlers/grpc/test/handler.spec.ts | 38 +++++- .../legacy/handlers/grpc/yaml-config.graphql | 5 +- .../handlers/openapi/yaml-config.graphql | 4 +- .../legacy/handlers/raml/yaml-config.graphql | 4 +- packages/legacy/types/src/config-schema.json | 32 ++--- packages/legacy/types/src/config.ts | 29 ++-- packages/loaders/grpc/package.json | 1 + packages/loaders/grpc/src/grpcLoaderHelper.ts | 34 ++++- .../getJSONSchemaOptionsFromOpenAPIOptions.ts | 7 +- packages/loaders/openapi/src/types.ts | 4 +- .../getJSONSchemaOptionsFromRAMLOptions.ts | 4 +- packages/loaders/raml/src/types.ts | 4 +- .../GrpcHandler.generated.md | 3 + website/src/pages/v1/source-handlers/grpc.mdx | 17 +++ yarn.lock | 1 + 17 files changed, 298 insertions(+), 59 deletions(-) create mode 100644 .changeset/large-oranges-draw.md diff --git a/.changeset/large-oranges-draw.md b/.changeset/large-oranges-draw.md new file mode 100644 index 0000000000000..2a2b82f42fda9 --- /dev/null +++ b/.changeset/large-oranges-draw.md @@ -0,0 +1,46 @@ +--- +'@graphql-mesh/openapi': patch +'@graphql-mesh/grpc': patch +'@graphql-mesh/raml': patch +'@omnigraph/openapi': patch +'@graphql-mesh/types': patch +'@omnigraph/grpc': patch +'@omnigraph/raml': patch +--- + +New option `selectQueryOrMutationField` to decide which field belongs to which root type explicitly. + +```ts filename="mesh.config.ts" +import loadGrpcSubgraph from '@omnigraph/grpc' +import { defineConfig } from '@graphql-mesh/compose-cli' + +export const composeConfig = defineConfig({ + subgraphs: [ + { + sourceHandler: loadGrpcSubgraph('MyGrpcApi', { + /** .. **/ + + // Prefix to collect Query method default: list, get + prefixQueryMethod: ['list', 'get'], + + // Select certain fields as Query or Mutation + // This overrides `prefixQueryMethod` + selectQueryOrMutationField: [ + { + // You can use a pattern matching with * + fieldName: '*RetrieveMovies', + type: 'Query', + }, + // Or you can use a specific field name + // This will make the field GetMovie available as a Mutation + // Because it would be Query because of `prefixQueryMethod` + { + fieldName: 'GetMovie', + type: 'Mutation' + } + ] + }) + } + ] +}); +``` diff --git a/packages/legacy/handlers/grpc/test/__snapshots__/handler.spec.ts.snap b/packages/legacy/handlers/grpc/test/__snapshots__/handler.spec.ts.snap index 04e3aa13e73f5..094ee7b2b2d0e 100644 --- a/packages/legacy/handlers/grpc/test/__snapshots__/handler.spec.ts.snap +++ b/packages/legacy/handlers/grpc/test/__snapshots__/handler.spec.ts.snap @@ -1102,7 +1102,7 @@ type Subscription { scalar TransportOptions" `; -exports[`gRPC Handler Load proto with prefixQueryMethod should load the retrieve-movie.proto 1`] = ` +exports[`gRPC Handler Load proto with prefixQueryMethod and selectQueryOrMutationField should load the retrieve-movie.proto with prefixQueryMethod 1`] = ` "schema @transport(subgraph: "prefixQueryMethod", kind: "grpc", location: "localhost", options: {requestTimeout: 200000, roots: [{name: "Root0", rootJson: "{\\"options\\":{\\"syntax\\":\\"proto3\\"},\\"nested\\":{\\"io\\":{\\"nested\\":{\\"xtech\\":{\\"nested\\":{\\"Genre\\":{\\"values\\":{\\"UNSPECIFIED\\":0,\\"ACTION\\":1,\\"DRAMA\\":2},\\"comment\\":null,\\"comments\\":{\\"UNSPECIFIED\\":null,\\"ACTION\\":null,\\"DRAMA\\":null}},\\"Movie\\":{\\"fields\\":{\\"name\\":{\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null},\\"year\\":{\\"type\\":\\"int32\\",\\"id\\":2,\\"comment\\":null},\\"rating\\":{\\"type\\":\\"float\\",\\"id\\":3,\\"comment\\":null},\\"cast\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"string\\",\\"id\\":4,\\"comment\\":\\"list of cast\\"},\\"time\\":{\\"type\\":\\"google.protobuf.Timestamp\\",\\"id\\":5,\\"comment\\":null},\\"genre\\":{\\"type\\":\\"Genre\\",\\"id\\":6,\\"comment\\":null}},\\"comment\\":\\"movie message payload\\"},\\"EmptyRequest\\":{\\"fields\\":{},\\"comment\\":null},\\"movie_request\\":{\\"fields\\":{\\"movie\\":{\\"type\\":\\"Movie\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"movie_request_by_ids\\":{\\"fields\\":{\\"movieIds\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"SearchByCastRequest\\":{\\"fields\\":{\\"castName\\":{\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"MoviesResult\\":{\\"fields\\":{\\"result\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"Movie\\",\\"id\\":1,\\"comment\\":\\"list of movies\\"}},\\"comment\\":\\"movie result message, contains list of movies\\"},\\"Example\\":{\\"methods\\":{\\"GetMovies\\":{\\"requestType\\":\\"movie_request\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get all movies\\"},\\"RetrieveMovies\\":{\\"requestType\\":\\"movie_request_by_ids\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get movies\\"},\\"SearchMoviesByCast\\":{\\"requestType\\":\\"SearchByCastRequest\\",\\"responseType\\":\\"Movie\\",\\"responseStream\\":true,\\"comment\\":\\"search movies by the name of the cast\\"}},\\"comment\\":null},\\"AnotherExample\\":{\\"methods\\":{\\"GetMovies\\":{\\"requestType\\":\\"movie_request\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get all movies\\"},\\"RetrieveMovies\\":{\\"requestType\\":\\"movie_request_by_ids\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get movies\\"},\\"SearchMoviesByCast\\":{\\"requestType\\":\\"SearchByCastRequest\\",\\"responseType\\":\\"Movie\\",\\"responseStream\\":true,\\"comment\\":\\"search movies by the name of the cast\\"}},\\"comment\\":null}}}}},\\"google\\":{\\"nested\\":{\\"protobuf\\":{\\"nested\\":{\\"Timestamp\\":{\\"fields\\":{\\"seconds\\":{\\"type\\":\\"int64\\",\\"id\\":1},\\"nanos\\":{\\"type\\":\\"int32\\",\\"id\\":2}},\\"comment\\":null}}}}}}}"}]}) { query: Query mutation: Mutation @@ -1227,3 +1227,125 @@ type Subscription { scalar TransportOptions" `; + +exports[`gRPC Handler Load proto with prefixQueryMethod and selectQueryOrMutationField should load the retrieve-movie.proto with selectQueryOrMutationField 1`] = ` +"schema @transport(subgraph: "selectQueryOrMutationField", kind: "grpc", location: "localhost", options: {requestTimeout: 200000, roots: [{name: "Root0", rootJson: "{\\"options\\":{\\"syntax\\":\\"proto3\\"},\\"nested\\":{\\"io\\":{\\"nested\\":{\\"xtech\\":{\\"nested\\":{\\"Genre\\":{\\"values\\":{\\"UNSPECIFIED\\":0,\\"ACTION\\":1,\\"DRAMA\\":2},\\"comment\\":null,\\"comments\\":{\\"UNSPECIFIED\\":null,\\"ACTION\\":null,\\"DRAMA\\":null}},\\"Movie\\":{\\"fields\\":{\\"name\\":{\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null},\\"year\\":{\\"type\\":\\"int32\\",\\"id\\":2,\\"comment\\":null},\\"rating\\":{\\"type\\":\\"float\\",\\"id\\":3,\\"comment\\":null},\\"cast\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"string\\",\\"id\\":4,\\"comment\\":\\"list of cast\\"},\\"time\\":{\\"type\\":\\"google.protobuf.Timestamp\\",\\"id\\":5,\\"comment\\":null},\\"genre\\":{\\"type\\":\\"Genre\\",\\"id\\":6,\\"comment\\":null}},\\"comment\\":\\"movie message payload\\"},\\"EmptyRequest\\":{\\"fields\\":{},\\"comment\\":null},\\"movie_request\\":{\\"fields\\":{\\"movie\\":{\\"type\\":\\"Movie\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"movie_request_by_ids\\":{\\"fields\\":{\\"movieIds\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"SearchByCastRequest\\":{\\"fields\\":{\\"castName\\":{\\"type\\":\\"string\\",\\"id\\":1,\\"comment\\":null}},\\"comment\\":null},\\"MoviesResult\\":{\\"fields\\":{\\"result\\":{\\"rule\\":\\"repeated\\",\\"type\\":\\"Movie\\",\\"id\\":1,\\"comment\\":\\"list of movies\\"}},\\"comment\\":\\"movie result message, contains list of movies\\"},\\"Example\\":{\\"methods\\":{\\"GetMovies\\":{\\"requestType\\":\\"movie_request\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get all movies\\"},\\"RetrieveMovies\\":{\\"requestType\\":\\"movie_request_by_ids\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get movies\\"},\\"SearchMoviesByCast\\":{\\"requestType\\":\\"SearchByCastRequest\\",\\"responseType\\":\\"Movie\\",\\"responseStream\\":true,\\"comment\\":\\"search movies by the name of the cast\\"}},\\"comment\\":null},\\"AnotherExample\\":{\\"methods\\":{\\"GetMovies\\":{\\"requestType\\":\\"movie_request\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get all movies\\"},\\"RetrieveMovies\\":{\\"requestType\\":\\"movie_request_by_ids\\",\\"responseType\\":\\"MoviesResult\\",\\"comment\\":\\"get movies\\"},\\"SearchMoviesByCast\\":{\\"requestType\\":\\"SearchByCastRequest\\",\\"responseType\\":\\"Movie\\",\\"responseStream\\":true,\\"comment\\":\\"search movies by the name of the cast\\"}},\\"comment\\":null}}}}},\\"google\\":{\\"nested\\":{\\"protobuf\\":{\\"nested\\":{\\"Timestamp\\":{\\"fields\\":{\\"seconds\\":{\\"type\\":\\"int64\\",\\"id\\":1},\\"nanos\\":{\\"type\\":\\"int32\\",\\"id\\":2}},\\"comment\\":null}}}}}}}"}]}) { + query: Query + subscription: Subscription +} + +directive @enum(subgraph: String, value: String) on ENUM_VALUE + +directive @grpcMethod(subgraph: String, rootJsonName: String, objPath: String, methodName: String, responseStream: Boolean) on FIELD_DEFINITION + +directive @grpcConnectivityState(subgraph: String, rootJsonName: String, objPath: String) on FIELD_DEFINITION + +""" +Directs the executor to stream plural fields when the \`if\` argument is true or undefined. +""" +directive @stream( + """Stream when true or undefined.""" + if: Boolean! = true + """Unique name""" + label: String + """Number of items to return immediately""" + initialCount: Int = 0 +) on FIELD + +directive @transport(subgraph: String, kind: String, location: String, options: TransportOptions) repeatable on SCHEMA + +type Query { + """get all movies""" + io_xtech_Example_GetMovies(input: io__xtech__movie_request_Input): io__xtech__MoviesResult @grpcMethod(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.Example", methodName: "GetMovies", responseStream: false) + """get movies""" + io_xtech_Example_RetrieveMovies(input: io__xtech__movie_request_by_ids_Input): io__xtech__MoviesResult @grpcMethod(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.Example", methodName: "RetrieveMovies", responseStream: false) + """search movies by the name of the cast""" + io_xtech_Example_SearchMoviesByCast(input: io__xtech__SearchByCastRequest_Input): [io__xtech__Movie] @grpcMethod(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.Example", methodName: "SearchMoviesByCast", responseStream: true) + io_xtech_Example_connectivityState(tryToConnect: Boolean): ConnectivityState @grpcConnectivityState(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.Example") + """get all movies""" + io_xtech_AnotherExample_GetMovies(input: io__xtech__movie_request_Input): io__xtech__MoviesResult @grpcMethod(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.AnotherExample", methodName: "GetMovies", responseStream: false) + """get movies""" + io_xtech_AnotherExample_RetrieveMovies(input: io__xtech__movie_request_by_ids_Input): io__xtech__MoviesResult @grpcMethod(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.AnotherExample", methodName: "RetrieveMovies", responseStream: false) + """search movies by the name of the cast""" + io_xtech_AnotherExample_SearchMoviesByCast(input: io__xtech__SearchByCastRequest_Input): [io__xtech__Movie] @grpcMethod(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.AnotherExample", methodName: "SearchMoviesByCast", responseStream: true) + io_xtech_AnotherExample_connectivityState(tryToConnect: Boolean): ConnectivityState @grpcConnectivityState(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.AnotherExample") +} + +"""movie result message, contains list of movies""" +type io__xtech__MoviesResult { + """list of movies""" + result: [io__xtech__Movie] +} + +"""movie message payload""" +type io__xtech__Movie { + name: String + year: Int + rating: Float + """list of cast""" + cast: [String] + time: google__protobuf__Timestamp + genre: io__xtech__Genre +} + +type google__protobuf__Timestamp { + seconds: BigInt + nanos: Int +} + +""" +The \`BigInt\` scalar type represents non-fractional signed whole numeric values. +""" +scalar BigInt + +enum io__xtech__Genre { + UNSPECIFIED @enum(subgraph: "selectQueryOrMutationField", value: "0") + ACTION @enum(subgraph: "selectQueryOrMutationField", value: "1") + DRAMA @enum(subgraph: "selectQueryOrMutationField", value: "2") +} + +input io__xtech__movie_request_Input { + movie: io__xtech__Movie_Input +} + +"""movie message payload""" +input io__xtech__Movie_Input { + name: String + year: Int + rating: Float + """list of cast""" + cast: [String] + time: google__protobuf__Timestamp_Input + genre: io__xtech__Genre +} + +input google__protobuf__Timestamp_Input { + seconds: BigInt + nanos: Int +} + +input io__xtech__movie_request_by_ids_Input { + movieIds: [String] +} + +input io__xtech__SearchByCastRequest_Input { + castName: String +} + +enum ConnectivityState { + IDLE + CONNECTING + READY + TRANSIENT_FAILURE + SHUTDOWN +} + +type Subscription { + """search movies by the name of the cast""" + io_xtech_Example_SearchMoviesByCast(input: io__xtech__SearchByCastRequest_Input): io__xtech__Movie @grpcMethod(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.Example", methodName: "SearchMoviesByCast", responseStream: true) + """search movies by the name of the cast""" + io_xtech_AnotherExample_SearchMoviesByCast(input: io__xtech__SearchByCastRequest_Input): io__xtech__Movie @grpcMethod(subgraph: "selectQueryOrMutationField", rootJsonName: "Root0", objPath: "io.xtech.AnotherExample", methodName: "SearchMoviesByCast", responseStream: true) +} + +scalar TransportOptions" +`; diff --git a/packages/legacy/handlers/grpc/test/handler.spec.ts b/packages/legacy/handlers/grpc/test/handler.spec.ts index 5c3fff64b8183..a02937b80721c 100644 --- a/packages/legacy/handlers/grpc/test/handler.spec.ts +++ b/packages/legacy/handlers/grpc/test/handler.spec.ts @@ -68,8 +68,8 @@ describe('gRPC Handler', () => { }); }); - describe('Load proto with prefixQueryMethod', () => { - test(`should load the retrieve-movie.proto`, async () => { + describe('Load proto with prefixQueryMethod and selectQueryOrMutationField', () => { + test(`should load the retrieve-movie.proto with prefixQueryMethod`, async () => { const file = 'retrieve-movie.proto'; const config: YamlConfig.GrpcHandler = { endpoint: 'localhost', @@ -93,6 +93,40 @@ describe('gRPC Handler', () => { const { schema } = await handler.getMeshSource(); + expect(schema).toBeInstanceOf(GraphQLSchema); + expect(validateSchema(schema)).toHaveLength(0); + expect(printSchemaWithDirectives(schema)).toContain('AnotherExample_RetrieveMovies'); + expect(printSchemaWithDirectives(schema)).toMatchSnapshot(); + }); + test(`should load the retrieve-movie.proto with selectQueryOrMutationField`, async () => { + const file = 'retrieve-movie.proto'; + const config: YamlConfig.GrpcHandler = { + endpoint: 'localhost', + source: { + file: join(__dirname, './fixtures/proto-tests', file), + load: { includeDirs: [join(__dirname, './fixtures/proto-tests')] }, + }, + selectQueryOrMutationField: [ + { + fieldName: '*RetrieveMovies', + type: 'Query', + }, + ], + }; + using cache = new InMemoryLRUCache(); + const handler = new GrpcHandler({ + name: 'selectQueryOrMutationField', + config, + cache, + pubsub, + store, + logger, + importFn: defaultImportFn, + baseDir: __dirname, + }); + + const { schema } = await handler.getMeshSource(); + expect(schema).toBeInstanceOf(GraphQLSchema); expect(validateSchema(schema)).toHaveLength(0); expect(printSchemaWithDirectives(schema)).toContain('AnotherExample_RetrieveMovies'); diff --git a/packages/legacy/handlers/grpc/yaml-config.graphql b/packages/legacy/handlers/grpc/yaml-config.graphql index 488b3af1c1e9c..e0fe83e4b0ae8 100644 --- a/packages/legacy/handlers/grpc/yaml-config.graphql +++ b/packages/legacy/handlers/grpc/yaml-config.graphql @@ -37,7 +37,10 @@ type GrpcHandler @md { prefix to collect Query method default: list, get """ prefixQueryMethod: [String] - + """ + Allows to explicitly override the default operation (Query or Mutation) for any gRPC operation + """ + selectQueryOrMutationField: [SelectQueryOrMutationFieldConfig] schemaHeaders: JSON } diff --git a/packages/legacy/handlers/openapi/yaml-config.graphql b/packages/legacy/handlers/openapi/yaml-config.graphql index 86db54dceeb5c..5f0cacbc0afc0 100644 --- a/packages/legacy/handlers/openapi/yaml-config.graphql +++ b/packages/legacy/handlers/openapi/yaml-config.graphql @@ -36,7 +36,7 @@ type OpenapiHandler @md { """ Allows to explicitly override the default operation (Query or Mutation) for any OAS operation """ - selectQueryOrMutationField: [OASSelectQueryOrMutationFieldConfig] + selectQueryOrMutationField: [SelectQueryOrMutationFieldConfig] """ JSON object representing the query search parameters to add to the API calls """ @@ -62,7 +62,7 @@ enum QueryOrMutation { Mutation } -type OASSelectQueryOrMutationFieldConfig { +type SelectQueryOrMutationFieldConfig { type: QueryOrMutation! fieldName: String! } diff --git a/packages/legacy/handlers/raml/yaml-config.graphql b/packages/legacy/handlers/raml/yaml-config.graphql index 4fcb08ba77204..5802de75e10c1 100644 --- a/packages/legacy/handlers/raml/yaml-config.graphql +++ b/packages/legacy/handlers/raml/yaml-config.graphql @@ -8,7 +8,7 @@ type RAMLHandler { schemaHeaders: JSON operationHeaders: JSON ignoreErrorResponses: Boolean - selectQueryOrMutationField: [RAMLSelectQueryOrMutationFieldConfig] + selectQueryOrMutationField: [SelectQueryOrMutationFieldConfig] queryParams: Any """ @@ -24,7 +24,7 @@ enum QueryOrMutation { Mutation } -type RAMLSelectQueryOrMutationFieldConfig { +type SelectQueryOrMutationFieldConfig { type: QueryOrMutation! fieldName: String! } diff --git a/packages/legacy/types/src/config-schema.json b/packages/legacy/types/src/config-schema.json index ad6d77bdc9a89..bd70fb5869223 100644 --- a/packages/legacy/types/src/config-schema.json +++ b/packages/legacy/types/src/config-schema.json @@ -963,6 +963,14 @@ "additionalItems": false, "description": "prefix to collect Query method default: list, get" }, + "selectQueryOrMutationField": { + "type": "array", + "items": { + "$ref": "#/definitions/SelectQueryOrMutationFieldConfig" + }, + "additionalItems": false, + "description": "Allows to explicitly override the default operation (Query or Mutation) for any gRPC operation" + }, "schemaHeaders": { "type": "object", "properties": {} @@ -1980,7 +1988,7 @@ "selectQueryOrMutationField": { "type": "array", "items": { - "$ref": "#/definitions/OASSelectQueryOrMutationFieldConfig" + "$ref": "#/definitions/SelectQueryOrMutationFieldConfig" }, "additionalItems": false, "description": "Allows to explicitly override the default operation (Query or Mutation) for any OAS operation" @@ -1997,10 +2005,10 @@ }, "required": ["source"] }, - "OASSelectQueryOrMutationFieldConfig": { + "SelectQueryOrMutationFieldConfig": { "additionalProperties": false, "type": "object", - "title": "OASSelectQueryOrMutationFieldConfig", + "title": "SelectQueryOrMutationFieldConfig", "properties": { "type": { "type": "string", @@ -2933,7 +2941,7 @@ "selectQueryOrMutationField": { "type": "array", "items": { - "$ref": "#/definitions/RAMLSelectQueryOrMutationFieldConfig" + "$ref": "#/definitions/SelectQueryOrMutationFieldConfig" }, "additionalItems": false }, @@ -2959,22 +2967,6 @@ }, "required": ["source"] }, - "RAMLSelectQueryOrMutationFieldConfig": { - "additionalProperties": false, - "type": "object", - "title": "RAMLSelectQueryOrMutationFieldConfig", - "properties": { - "type": { - "type": "string", - "enum": ["query", "mutation", "Query", "Mutation"], - "description": "Allowed values: query, mutation, Query, Mutation" - }, - "fieldName": { - "type": "string" - } - }, - "required": ["type", "fieldName"] - }, "SoapHandler": { "additionalProperties": false, "type": "object", diff --git a/packages/legacy/types/src/config.ts b/packages/legacy/types/src/config.ts index 19ecf8ea41bab..9b7b7bcea6a83 100644 --- a/packages/legacy/types/src/config.ts +++ b/packages/legacy/types/src/config.ts @@ -334,6 +334,10 @@ export interface GrpcHandler { * prefix to collect Query method default: list, get */ prefixQueryMethod?: string[]; + /** + * Allows to explicitly override the default operation (Query or Mutation) for any gRPC operation + */ + selectQueryOrMutationField?: SelectQueryOrMutationFieldConfig[]; schemaHeaders?: { [k: string]: any; }; @@ -354,6 +358,13 @@ export interface GrpcCredentialsSsl { certChain?: string; privateKey?: string; } +export interface SelectQueryOrMutationFieldConfig { + /** + * Allowed values: query, mutation, Query, Mutation + */ + type: 'query' | 'mutation' | 'Query' | 'Mutation'; + fieldName: string; +} /** * Handler for JSON Schema specification. * Source could be a local json file, or a url to it. @@ -908,7 +919,7 @@ export interface OpenapiHandler { /** * Allows to explicitly override the default operation (Query or Mutation) for any OAS operation */ - selectQueryOrMutationField?: OASSelectQueryOrMutationFieldConfig[]; + selectQueryOrMutationField?: SelectQueryOrMutationFieldConfig[]; /** * JSON object representing the query search parameters to add to the API calls */ @@ -920,13 +931,6 @@ export interface OpenapiHandler { */ timeout?: number; } -export interface OASSelectQueryOrMutationFieldConfig { - /** - * Allowed values: query, mutation, Query, Mutation - */ - type: 'query' | 'mutation' | 'Query' | 'Mutation'; - fieldName: string; -} /** * Handler for Postgres database, based on `postgraphile` */ @@ -982,20 +986,13 @@ export interface RAMLHandler { [k: string]: any; }; ignoreErrorResponses?: boolean; - selectQueryOrMutationField?: RAMLSelectQueryOrMutationFieldConfig[]; + selectQueryOrMutationField?: SelectQueryOrMutationFieldConfig[]; queryParams?: any; /** * Timeout for the HTTP request in milliseconds */ timeout?: number; } -export interface RAMLSelectQueryOrMutationFieldConfig { - /** - * Allowed values: query, mutation, Query, Mutation - */ - type: 'query' | 'mutation' | 'Query' | 'Mutation'; - fieldName: string; -} /** * Handler for SOAP */ diff --git a/packages/loaders/grpc/package.json b/packages/loaders/grpc/package.json index 8953d928ea028..888d589eb994b 100644 --- a/packages/loaders/grpc/package.json +++ b/packages/loaders/grpc/package.json @@ -46,6 +46,7 @@ "graphql-compose": "^9.1.0", "graphql-scalars": "^1.23.0", "lodash.has": "^4.5.2", + "micromatch": "^4.0.8", "protobufjs": "^7.2.5" }, "publishConfig": { diff --git a/packages/loaders/grpc/src/grpcLoaderHelper.ts b/packages/loaders/grpc/src/grpcLoaderHelper.ts index 80d156d1e0b41..328947f72ecaf 100644 --- a/packages/loaders/grpc/src/grpcLoaderHelper.ts +++ b/packages/loaders/grpc/src/grpcLoaderHelper.ts @@ -1,6 +1,7 @@ import globby from 'globby'; import { specifiedDirectives } from 'graphql'; import { + type ObjectTypeComposer, SchemaComposer, type Directive, type EnumTypeComposerValueConfigDefinition, @@ -13,6 +14,7 @@ import { GraphQLUnsignedInt, GraphQLVoid, } from 'graphql-scalars'; +import micromatch from 'micromatch'; import protobufjs, { type AnyNestedObject, type IParseOptions, @@ -482,11 +484,33 @@ export class GrpcLoaderHelper extends DisposableStack { fieldConfig.args = fieldConfigArgs; const methodNameLowerCased = methodName.toLowerCase(); const prefixQueryMethod = this.config.prefixQueryMethod || QUERY_METHOD_PREFIXES; - const rootTypeComposer = prefixQueryMethod.some(prefix => - methodNameLowerCased.startsWith(prefix), - ) - ? this.schemaComposer.Query - : this.schemaComposer.Mutation; + let rootTypeComposer: ObjectTypeComposer; + if (this.config.selectQueryOrMutationField) { + const selection = this.config.selectQueryOrMutationField.find( + selection => micromatch([rootFieldName], selection.fieldName).length > 0, + ); + const rootTypeName = selection?.type?.toLowerCase(); + if (rootTypeName) { + if (rootTypeName === 'query') { + rootTypeComposer = this.schemaComposer.Query; + } else if (rootTypeName === 'mutation') { + rootTypeComposer = this.schemaComposer.Mutation; + } else if (rootTypeName === 'subscription') { + rootTypeComposer = this.schemaComposer.Subscription; + } else { + throw new Error( + `Unknown type provided ${selection.type} for ${rootFieldName}; available options are Query, Mutation and Subscription`, + ); + } + } + } + if (rootTypeComposer == null) { + rootTypeComposer = prefixQueryMethod.some(prefix => + methodNameLowerCased.startsWith(prefix), + ) + ? this.schemaComposer.Query + : this.schemaComposer.Mutation; + } this.schemaComposer.addDirective(grpcMethodDirective); rootTypeComposer.addFields({ [rootFieldName]: { diff --git a/packages/loaders/openapi/src/getJSONSchemaOptionsFromOpenAPIOptions.ts b/packages/loaders/openapi/src/getJSONSchemaOptionsFromOpenAPIOptions.ts index 1aa4f95da2b8f..63523735a1561 100644 --- a/packages/loaders/openapi/src/getJSONSchemaOptionsFromOpenAPIOptions.ts +++ b/packages/loaders/openapi/src/getJSONSchemaOptionsFromOpenAPIOptions.ts @@ -25,7 +25,7 @@ import type { JSONSchemaPubSubOperationConfig, OperationHeadersConfiguration, } from '@omnigraph/json-schema'; -import type { OpenAPILoaderSelectQueryOrMutationFieldConfig } from './types.js'; +import type { SelectQueryOrMutationFieldConfig } from './types.js'; import { getFieldNameFromPath } from './utils.js'; export interface HATEOASConfig { @@ -63,7 +63,7 @@ interface GetJSONSchemaOptionsFromOpenAPIOptionsParams { schemaHeaders?: Record; operationHeaders?: OperationHeadersConfiguration; queryParams?: Record; - selectQueryOrMutationField?: OpenAPILoaderSelectQueryOrMutationFieldConfig[]; + selectQueryOrMutationField?: SelectQueryOrMutationFieldConfig[]; logger?: Logger; jsonApi?: boolean; HATEOAS?: Partial | boolean; @@ -115,8 +115,7 @@ export async function getJSONSchemaOptionsFromOpenAPIOptions( env: process.env, }); } - const fieldTypeMap: Record = - {}; + const fieldTypeMap: Record = {}; for (const { fieldName, type } of selectQueryOrMutationField) { fieldTypeMap[fieldName] = type; } diff --git a/packages/loaders/openapi/src/types.ts b/packages/loaders/openapi/src/types.ts index 36c86cc2934f9..0988abaaebfc0 100644 --- a/packages/loaders/openapi/src/types.ts +++ b/packages/loaders/openapi/src/types.ts @@ -4,13 +4,13 @@ import type { HATEOASConfig } from './getJSONSchemaOptionsFromOpenAPIOptions.js' export interface OpenAPILoaderOptions extends Partial { // The URL or FileSystem path to the OpenAPI Document. source: string; - selectQueryOrMutationField?: OpenAPILoaderSelectQueryOrMutationFieldConfig[]; + selectQueryOrMutationField?: SelectQueryOrMutationFieldConfig[]; fallbackFormat?: 'json' | 'yaml' | 'js' | 'ts'; jsonApi?: boolean; HATEOAS?: HATEOASConfig | boolean; } -export interface OpenAPILoaderSelectQueryOrMutationFieldConfig { +export interface SelectQueryOrMutationFieldConfig { type: 'query' | 'mutation' | 'Query' | 'Mutation'; fieldName: string; } diff --git a/packages/loaders/raml/src/getJSONSchemaOptionsFromRAMLOptions.ts b/packages/loaders/raml/src/getJSONSchemaOptionsFromRAMLOptions.ts index 29139a56ee8ef..c6ff0b97364a4 100644 --- a/packages/loaders/raml/src/getJSONSchemaOptionsFromRAMLOptions.ts +++ b/packages/loaders/raml/src/getJSONSchemaOptionsFromRAMLOptions.ts @@ -14,7 +14,7 @@ import type { JSONSchemaOperationResponseConfig, } from '@omnigraph/json-schema'; import { fetch as crossUndiciFetch } from '@whatwg-node/fetch'; -import type { RAMLLoaderOptions, RAMLLoaderSelectQueryOrMutationFieldConfig } from './types.js'; +import type { RAMLLoaderOptions, SelectQueryOrMutationFieldConfig } from './types.js'; import { getFieldNameFromPath } from './utils.js'; function resolveTraitsByIs(base: { is: () => api10.TraitRef[] }) { @@ -48,7 +48,7 @@ export async function getJSONSchemaOptionsFromRAMLOptions({ endpoint: string; fetch?: MeshFetch; }> { - const fieldTypeMap: Record = {}; + const fieldTypeMap: Record = {}; for (const { fieldName, type } of selectQueryOrMutationField) { fieldTypeMap[fieldName] = type; } diff --git a/packages/loaders/raml/src/types.ts b/packages/loaders/raml/src/types.ts index 83f345a7002e4..6e26383a740d9 100644 --- a/packages/loaders/raml/src/types.ts +++ b/packages/loaders/raml/src/types.ts @@ -2,10 +2,10 @@ import type { JSONSchemaLoaderOptions } from '@omnigraph/json-schema'; export interface RAMLLoaderOptions extends Partial { source: string; - selectQueryOrMutationField?: RAMLLoaderSelectQueryOrMutationFieldConfig[]; + selectQueryOrMutationField?: SelectQueryOrMutationFieldConfig[]; } -export interface RAMLLoaderSelectQueryOrMutationFieldConfig { +export interface SelectQueryOrMutationFieldConfig { type: 'query' | 'mutation' | 'Query' | 'Mutation'; fieldName: string; } diff --git a/website/src/generated-markdown/GrpcHandler.generated.md b/website/src/generated-markdown/GrpcHandler.generated.md index c58c271a62b18..1b4936ca50843 100644 --- a/website/src/generated-markdown/GrpcHandler.generated.md +++ b/website/src/generated-markdown/GrpcHandler.generated.md @@ -18,4 +18,7 @@ Default: 200000 * `useHTTPS` (type: `Boolean`) - Use https instead of http for gRPC connection * `metaData` (type: `JSON`) - MetaData * `prefixQueryMethod` (type: `Array of String`) - prefix to collect Query method default: list, get +* `selectQueryOrMutationField` (type: `Array of Object`) - Allows to explicitly override the default operation (Query or Mutation) for any gRPC operation: + * `type` (type: `String (query | mutation | Query | Mutation)`, required) + * `fieldName` (type: `String`, required) * `schemaHeaders` (type: `JSON`) \ No newline at end of file diff --git a/website/src/pages/v1/source-handlers/grpc.mdx b/website/src/pages/v1/source-handlers/grpc.mdx index aecbd2a1b4c46..e1e2a94d77178 100644 --- a/website/src/pages/v1/source-handlers/grpc.mdx +++ b/website/src/pages/v1/source-handlers/grpc.mdx @@ -58,6 +58,23 @@ export const composeConfig = defineConfig({ // Prefix to collect Query method default: list, get prefixQueryMethod: ['list', 'get'], + // Select certain fields as Query or Mutation + // This overrides `prefixQueryMethod` + selectQueryOrMutationField: [ + { + // You can use a pattern matching with * + fieldName: '*RetrieveMovies', + type: 'Query', + }, + // Or you can use a specific field name + // This will make the field GetMovie available as a Mutation + // Because it would be Query because of `prefixQueryMethod` + { + fieldName: 'GetMovie', + type: 'Mutation' + } + ] + // Headers for the protobuf if URL is provided schemaHeaders: { 'x-api-key': 'my-api-key' diff --git a/yarn.lock b/yarn.lock index a06f8994d3087..aaf5f5b03acc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10329,6 +10329,7 @@ __metadata: graphql-compose: "npm:^9.1.0" graphql-scalars: "npm:^1.23.0" lodash.has: "npm:^4.5.2" + micromatch: "npm:^4.0.8" protobufjs: "npm:^7.2.5" peerDependencies: graphql: "*"