diff --git a/packages/openapi-to-graphql/src/index.ts b/packages/openapi-to-graphql/src/index.ts index 05a5c01e..21a47d38 100644 --- a/packages/openapi-to-graphql/src/index.ts +++ b/packages/openapi-to-graphql/src/index.ts @@ -104,6 +104,7 @@ const DEFAULT_OPTIONS: InternalOptions = { genericPayloadArgName: false, simpleNames: false, simpleEnumValues: false, + nonSanitizableObjectKeys: [], singularNames: false, createSubscriptionsFromCallbacks: false, @@ -188,6 +189,7 @@ export function translateOpenAPIToGraphQL( genericPayloadArgName, simpleNames, simpleEnumValues, + nonSanitizableObjectKeys, singularNames, createSubscriptionsFromCallbacks, @@ -230,6 +232,7 @@ export function translateOpenAPIToGraphQL( genericPayloadArgName, simpleNames, simpleEnumValues, + nonSanitizableObjectKeys, singularNames, createSubscriptionsFromCallbacks, diff --git a/packages/openapi-to-graphql/src/oas_3_tools.ts b/packages/openapi-to-graphql/src/oas_3_tools.ts index e79d738d..ddff900d 100644 --- a/packages/openapi-to-graphql/src/oas_3_tools.ts +++ b/packages/openapi-to-graphql/src/oas_3_tools.ts @@ -461,7 +461,8 @@ function buildUrl(server: ServerObject): string { */ export function sanitizeObjectKeys( obj: any, // obj does not necessarily need to be an object - caseStyle: CaseStyle = CaseStyle.camelCase + caseStyle: CaseStyle = CaseStyle.camelCase, + nonSanitizableObjectKeys?: string[] ): any { const cleanKeys = (obj: any): any => { // Case: no (response) data @@ -477,6 +478,11 @@ export function sanitizeObjectKeys( const res: object = {} for (const key in obj) { + // Escape hatch if the caller really does not want to have these keys sanitized + if (nonSanitizableObjectKeys && nonSanitizableObjectKeys.includes(key)) { + res[key] = obj[key] + continue + } const saneKey = sanitize(key, caseStyle) if (Object.prototype.hasOwnProperty.call(obj, key)) { diff --git a/packages/openapi-to-graphql/src/resolver_builder.ts b/packages/openapi-to-graphql/src/resolver_builder.ts index 94f7b42d..34dc6c16 100644 --- a/packages/openapi-to-graphql/src/resolver_builder.ts +++ b/packages/openapi-to-graphql/src/resolver_builder.ts @@ -832,7 +832,8 @@ export function getResolver({ responseBody, !data.options.simpleNames ? Oas3Tools.CaseStyle.camelCase - : Oas3Tools.CaseStyle.simple + : Oas3Tools.CaseStyle.simple, + data.options.nonSanitizableObjectKeys ) // Pass on _openAPIToGraphQL to subsequent resolvers diff --git a/packages/openapi-to-graphql/src/types/options.ts b/packages/openapi-to-graphql/src/types/options.ts index 3722bcd0..01b1201f 100644 --- a/packages/openapi-to-graphql/src/types/options.ts +++ b/packages/openapi-to-graphql/src/types/options.ts @@ -184,6 +184,15 @@ export type InternalOptions = { */ simpleEnumValues: boolean + /** + * By default, field names are sanitized to conform with GraphQL conventions, + * i.e. fields should only contain alphanumeric characters. + * + * This option will prevent OpenAPI-to-GraphQL from sanitizing objects falling + * under the provided keys. + */ + nonSanitizableObjectKeys?: string[] + /** * Experimental feature that will try to create more meaningful names from * the operation path than the response object by leveraging common diff --git a/packages/openapi-to-graphql/test/example_api6.test.ts b/packages/openapi-to-graphql/test/example_api6.test.ts index 5007f34a..412d9f7d 100644 --- a/packages/openapi-to-graphql/test/example_api6.test.ts +++ b/packages/openapi-to-graphql/test/example_api6.test.ts @@ -396,3 +396,31 @@ test('Handle no response schema', () => { }) }) }) + +/** + * Get /testDynamicallyKeyedObject has an object with a key we don't want + * to desanitize. + */ +test('Properties present', () => { + const query = `{ + dynamicallyKeyedObject{ + name + dynamic + } + }` + + const options: Options = { + nonSanitizableObjectKeys: ["dynamic"] + } + + return openAPIToGraphQL + .createGraphQLSchema(oas, options) + .then(({ schema, report }) => { + return graphql(schema, query).then((result) => { + expect(result.data.dynamicallyKeyedObject).toEqual({ + name: 'Mr Dynamic', + dynamic: { 'ぁ': '56', '1234567': 'legit' }, + }) + }) + }) +}) diff --git a/packages/openapi-to-graphql/test/example_api6_server.js b/packages/openapi-to-graphql/test/example_api6_server.js index 9e52ae11..7b3d7366 100644 --- a/packages/openapi-to-graphql/test/example_api6_server.js +++ b/packages/openapi-to-graphql/test/example_api6_server.js @@ -102,6 +102,12 @@ function startServer(PORT) { res.send({"nesting1": {"nesting2": 5} }) }) + app.get('/api/testDynamicallyKeyedObject', (req, res) => { + res.send({ + name: 'Mr Dynamic', + dynamic: { 'ぁ': '56', '1234567': 'legit' }, + }) + }) return new Promise((resolve) => { server = app.listen(PORT, () => { diff --git a/packages/openapi-to-graphql/test/fixtures/example_oas6.json b/packages/openapi-to-graphql/test/fixtures/example_oas6.json index 19aa17c3..e8bfda87 100644 --- a/packages/openapi-to-graphql/test/fixtures/example_oas6.json +++ b/packages/openapi-to-graphql/test/fixtures/example_oas6.json @@ -410,7 +410,24 @@ "operationId": "returnNumber", "parameters": { "number": "$response.body#/nesting1/nesting2" - } + } + } + } + } + } + } + }, + "/testDynamicallyKeyedObject": { + "get": { + "description": "Test object with dynamic keyes that shouldn't be sanitized.", + "responses": { + "200": { + "description": "A successful response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/dynamicallyKeyedObject" + } } } } @@ -465,6 +482,17 @@ "$ref": "#/components/schemas/russianDoll" } } + }, + "dynamicallyKeyedObject": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "dynamic": { + "type": "object" + } + } } }, "links": { diff --git a/packages/openapi-to-graphql/test/oas_3_tools.test.ts b/packages/openapi-to-graphql/test/oas_3_tools.test.ts index e39e6ccc..5792f463 100644 --- a/packages/openapi-to-graphql/test/oas_3_tools.test.ts +++ b/packages/openapi-to-graphql/test/oas_3_tools.test.ts @@ -75,6 +75,26 @@ test('Sanitize object keys when given an array', () => { ]) }) +test('Sanitize object keys with exceptions', () => { + const obj = [ + { + properties: { + 5353535353: 'test', + '££$£$': 'fine' + } + } + ] + const clean = Oas3Tools.sanitizeObjectKeys(obj, Oas3Tools.CaseStyle.camelCase, ["properties"]) + expect(clean).toEqual([ + { + properties: { + 5353535353: 'test', + '££$£$': 'fine' + } + } + ]) +}) + const mapping = { productId: 'product-id', productName: 'product-name',