Skip to content

Commit

Permalink
Merge pull request #72 from charlypoly/newhouse/38/support-id-scalar
Browse files Browse the repository at this point in the history
  • Loading branch information
newhouse authored Jan 31, 2023
2 parents 4e809f8 + 7498be1 commit d289487
Show file tree
Hide file tree
Showing 10 changed files with 570 additions and 431 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ typings/
# Output of 'npm pack'
*.tgz

# Yarn Integrity file
# Yarn stuff
yarn.lock
.yarn-integrity

# dotenv environment variables file
Expand Down
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ const options = {
// Defaults to `false` for backwards compatibility, but in future versions
// the effect of `true` is likely going to be the default and only way. It is
// highly recommended that new implementations set this value to `true`.
nullableArrayItems: true
nullableArrayItems: true,
// Indicates how to define the `ID` scalar as part of a JSON Schema. Valid options
// are `string`, `number`, or `both`. Defaults to `string`
idTypeMapping: 'string'
}

// schema is your GraphQL schema.
Expand All @@ -43,7 +46,7 @@ const jsonSchema = fromIntrospectionQuery(introspection, options);

```graphql
type Todo {
id: String!
id: ID!
name: String!
completed: Boolean
color: Color
Expand All @@ -55,7 +58,7 @@ const jsonSchema = fromIntrospectionQuery(introspection, options);
}

type SimpleTodo {
id: String!
id: ID!
name: String!
}

Expand All @@ -77,7 +80,7 @@ const jsonSchema = fromIntrospectionQuery(introspection, options);
type Query {
"A Query with 1 required argument and 1 optional argument"
todo(
id: String!,
id: ID!,
"A default value of false"
isCompleted: Boolean=false
): Todo
Expand All @@ -97,7 +100,7 @@ const jsonSchema = fromIntrospectionQuery(introspection, options);

"A Mutation with 2 required arguments"
update_todo(
id: String!,
id: ID!,
data: TodoInputType!
): Todo!

Expand Down Expand Up @@ -129,7 +132,7 @@ const options = { nullableArrayItems: true }
arguments: {
type: 'object',
properties: {
id: { '$ref': '#/definitions/String' },
id: { '$ref': '#/definitions/ID' },
isCompleted: {
description: 'A default value of false',
'$ref': '#/definitions/Boolean',
Expand Down Expand Up @@ -192,7 +195,7 @@ const options = { nullableArrayItems: true }
arguments: {
type: 'object',
properties: {
id: { '$ref': '#/definitions/String' },
id: { '$ref': '#/definitions/ID' },
data: { '$ref': '#/definitions/TodoInputType' }
},
required: [ 'id', 'data' ]
Expand Down Expand Up @@ -235,7 +238,7 @@ const options = { nullableArrayItems: true }
id: {
type: 'object',
properties: {
return: { '$ref': '#/definitions/String' },
return: { '$ref': '#/definitions/ID' },
arguments: { type: 'object', properties: {}, required: [] }
},
required: []
Expand Down Expand Up @@ -285,13 +288,18 @@ const options = { nullableArrayItems: true }
},
required: [ 'id', 'name', 'colors' ]
},
ID: {
description: 'The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.',
type: 'string',
title: 'ID'
},
SimpleTodo: {
type: 'object',
properties: {
id: {
type: 'object',
properties: {
return: { '$ref': '#/definitions/String' },
return: { '$ref': '#/definitions/ID' },
arguments: { type: 'object', properties: {}, required: [] }
},
required: []
Expand Down
29 changes: 29 additions & 0 deletions __tests__/spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import ajv from 'ajv'
import { JSONSchema6 } from 'json-schema'
import { fromIntrospectionQuery } from '../lib/fromIntrospectionQuery'
import type { IDTypeMapping as IDTypeMappingType } from '../lib/types'
import {
getTodoSchemaIntrospection,
todoSchemaAsJsonSchema,
todoSchemaAsJsonSchemaWithoutNullableArrayItems,
todoSchemaAsJsonSchemaWithIdTypeNumber,
todoSchemaAsJsonSchemaWithIdTypeStringOrNumber,
} from '../test-utils'

describe('GraphQL to JSON Schema', () => {
Expand All @@ -30,4 +33,30 @@ describe('GraphQL to JSON Schema', () => {
validator.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'))
expect(validator.validateSchema(result)).toBe(true)
})

test('from IntrospectionQuery object with idTypeMapping = "number"', () => {
const options = {
nullableArrayItems: true,
idTypeMapping: 'number' as IDTypeMappingType,
}
const result = fromIntrospectionQuery(introspection, options)
expect(result).toEqual(<JSONSchema6>todoSchemaAsJsonSchemaWithIdTypeNumber)
const validator = new ajv()
validator.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'))
expect(validator.validateSchema(result)).toBe(true)
})

test('from IntrospectionQuery object with idTypeMapping = "both"', () => {
const options = {
nullableArrayItems: true,
idTypeMapping: 'both' as IDTypeMappingType,
}
const result = fromIntrospectionQuery(introspection, options)
expect(result).toEqual(
<JSONSchema6>todoSchemaAsJsonSchemaWithIdTypeStringOrNumber
)
const validator = new ajv()
validator.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'))
expect(validator.validateSchema(result)).toBe(true)
})
})
8 changes: 4 additions & 4 deletions doc-exampleGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const nativeScalarsToFilter = ['String', 'Int', 'Boolean']

const readmeSDL: string = `
type Todo {
id: String!
id: ID!
name: String!
completed: Boolean
color: Color
Expand All @@ -26,7 +26,7 @@ const readmeSDL: string = `
}
type SimpleTodo {
id: String!
id: ID!
name: String!
}
Expand All @@ -48,7 +48,7 @@ const readmeSDL: string = `
type Query {
"A Query with 1 required argument and 1 optional argument"
todo(
id: String!,
id: ID!,
"A default value of false"
isCompleted: Boolean=false
): Todo
Expand All @@ -68,7 +68,7 @@ const readmeSDL: string = `
"A Mutation with 2 required arguments"
update_todo(
id: String!,
id: ID!,
data: TodoInputType!
): Todo!
Expand Down
10 changes: 9 additions & 1 deletion lib/fromIntrospectionQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { IntrospectionQuery, IntrospectionType } from 'graphql'
import { JSONSchema6 } from 'json-schema'
import { includes, partition, reduce } from 'lodash'
import { introspectionTypeReducer, JSONSchema6Acc } from './reducer'
import { filterDefinitionsTypes, isIntrospectionObjectType } from './typeGuards'
import {
ID_TYPE_MAPPING_OPTION_DEFAULT,
filterDefinitionsTypes,
isIntrospectionObjectType,
} from './typeGuards'

import type { IDTypeMapping as IDTypeMappingType } from './types'

// FIXME: finish this type
export interface GraphQLJSONSchema6 extends JSONSchema6 {
Expand All @@ -16,6 +22,7 @@ export interface GraphQLJSONSchema6 extends JSONSchema6 {
export interface FromIntrospectionQueryOptions {
ignoreInternals?: boolean
nullableArrayItems?: boolean
idTypeMapping?: IDTypeMappingType
}

export const fromIntrospectionQuery = (
Expand All @@ -26,6 +33,7 @@ export const fromIntrospectionQuery = (
// Defaults
ignoreInternals: true,
nullableArrayItems: false,
idTypeMapping: ID_TYPE_MAPPING_OPTION_DEFAULT,
// User-specified
...(opts || {}),
}
Expand Down
10 changes: 8 additions & 2 deletions lib/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,20 @@ import {
isIntrospectionScalarType,
isIntrospectionDefaultScalarType,
} from './typeGuards'
import { graphqlToJSONType, typesMapping } from './typesMapping'
import { graphqlToJSONType, scalarToJsonType } from './typesMapping'

import type {
GraphQLTypeNames,
IDTypeMapping as IDTypeMappingType,
} from './types'

export type JSONSchema6Acc = {
[k: string]: JSONSchema6
}

type ReducerOptions = {
nullableArrayItems?: boolean
idTypeMapping?: IDTypeMappingType
}

type GetRequiredFieldsType = ReadonlyArray<
Expand Down Expand Up @@ -199,7 +205,7 @@ export const introspectionTypeReducer: (
}
} else if (isIntrospectionDefaultScalarType(curr)) {
acc[curr.name] = {
type: (typesMapping as any)[curr.name],
type: scalarToJsonType(curr.name as GraphQLTypeNames, options),
title: curr.name,
}
} else if (isIntrospectionScalarType(curr)) {
Expand Down
17 changes: 13 additions & 4 deletions lib/typeGuards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,24 @@ import {
} from 'graphql'
import { filter, has, startsWith, includes } from 'lodash'

export const SUPPORTED_KINDS = [
export { ID_TYPE_MAPPING_OPTION_DEFAULT } from './typesMapping'

export const SUPPORTED_SCALARS = Object.freeze([
'Boolean',
'String',
'Int',
'Float',
'ID',
])

export const SUPPORTED_KINDS = Object.freeze([
TypeKind.SCALAR,
TypeKind.OBJECT,
TypeKind.INPUT_OBJECT,
TypeKind.INTERFACE,
TypeKind.ENUM,
TypeKind.UNION,
]
])

///////////////////
/// Type guards ///
Expand Down Expand Up @@ -81,8 +91,7 @@ export const isIntrospectionUnionType = (
export const isIntrospectionDefaultScalarType = (
type: IntrospectionSchema['types'][0]
): type is IntrospectionScalarType =>
type.kind === TypeKind.SCALAR &&
includes(['Boolean', 'String', 'Int', 'Float'], type.name)
type.kind === TypeKind.SCALAR && includes(SUPPORTED_SCALARS, type.name)

// Ignore all GraphQL native Scalars, directives, etc...
export interface FilterDefinitionsTypesOptions {
Expand Down
2 changes: 2 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type GraphQLTypeNames = 'String' | 'Int' | 'Float' | 'Boolean' | 'ID'
export type IDTypeMapping = 'string' | 'number' | 'both'
29 changes: 25 additions & 4 deletions lib/typesMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,36 @@ import {
isNonNullIntrospectionType,
} from './typeGuards'

export type GraphQLTypeNames = 'String' | 'Int' | 'Float' | 'Boolean' | 'ID'
import { GraphQLTypeNames, IDTypeMapping as IDTypeMappingType } from './types'

export const typesMapping: { [k in GraphQLTypeNames]: JSONSchema6TypeName } = {
export const ID_TYPE_MAPPING_OPTION_DEFAULT = 'string' as IDTypeMappingType

const ID_TYPES: {
[k in IDTypeMappingType]: JSONSchema6TypeName | JSONSchema6TypeName[]
} = {
string: 'string',
number: 'number',
both: ['string', 'number'],
}

const SCALAR_TO_JSON: {
[k in GraphQLTypeNames]: JSONSchema6TypeName | JSONSchema6TypeName[]
} = {
Boolean: 'boolean',
String: 'string',
Int: 'number',
Float: 'number',
ID: 'string',
ID: ID_TYPES[ID_TYPE_MAPPING_OPTION_DEFAULT],
}

export const scalarToJsonType = (
scalarName: GraphQLTypeNames,
options: GraphqlToJSONTypeOptions = {}
): JSONSchema6TypeName | JSONSchema6TypeName[] =>
Object.assign({}, SCALAR_TO_JSON, {
ID: ID_TYPES[options.idTypeMapping || ID_TYPE_MAPPING_OPTION_DEFAULT],
})[scalarName]

// Convert a GraphQL Type to a valid JSON Schema type
export type GraphqlToJSONTypeArg =
| IntrospectionTypeRef
Expand All @@ -34,6 +54,7 @@ export type GraphqlToJSONTypeOptions = {
nullableArrayItems?: boolean
isArray?: boolean
isNonNull?: boolean
idTypeMapping?: IDTypeMappingType
}

export const graphqlToJSONType = (
Expand All @@ -59,7 +80,7 @@ export const graphqlToJSONType = (
if (includes(SUPPORTED_KINDS, k.kind)) {
jsonType.$ref = `#/definitions/${name}`
} else {
jsonType.type = (typesMapping as any)[name]
jsonType.type = scalarToJsonType(name as GraphQLTypeNames, options)
}

// Only if the option allows for it, represent an array with nullable items
Expand Down
Loading

0 comments on commit d289487

Please sign in to comment.