-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(graphql): introduce GraphQL Converter
closes #237
- Loading branch information
Showing
79 changed files
with
4,057 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { ConverterOptions } from './ConverterOptions'; | ||
import { GraphQL, Request } from '@har-sdk/core'; | ||
|
||
export interface Converter { | ||
convert( | ||
envelope: GraphQL.Document, | ||
options: ConverterOptions | ||
): Promise<Request[]>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export class ConverterConstants { | ||
public static readonly OPERATION_COST_THRESHOLD = 100; | ||
public static readonly MAX_OPERATIONS_OUTPUT = 10_000; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export interface ConverterOptions { | ||
skipInPlaceValues?: boolean; | ||
skipExternalizedVariables?: boolean; | ||
skipFileUploads?: boolean; | ||
includeSimilarOperations?: boolean; | ||
limit?: number; | ||
operationCostThreshold?: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { Converter } from './Converter'; | ||
import { ConverterOptions } from './ConverterOptions'; | ||
import { | ||
Operation, | ||
Operations, | ||
DefaultOperations, | ||
OperationRequestBuilder | ||
} from './operations'; | ||
import { GraphQL, Request } from '@har-sdk/core'; | ||
|
||
export class DefaultConverter implements Converter { | ||
constructor( | ||
private readonly operations: Operations = new DefaultOperations(), | ||
private readonly requestBuilder: OperationRequestBuilder = new OperationRequestBuilder() | ||
) {} | ||
|
||
public async convert( | ||
doc: GraphQL.Document, | ||
options: ConverterOptions = {} | ||
): Promise<Request[]> { | ||
if (!this.isGraphQLDocument(doc)) { | ||
throw new TypeError('Please provide a valid GraphQL document.'); | ||
} | ||
|
||
const operations = this.operations.create(doc.data, options); | ||
|
||
return operations.map((operation: Operation) => | ||
this.requestBuilder.build({ operation, url: doc.url }) | ||
); | ||
} | ||
|
||
private isGraphQLDocument(obj: object): obj is GraphQL.Document { | ||
const hasValidUrl = | ||
'url' in obj && | ||
typeof (obj as GraphQL.Document).url === 'string' && | ||
this.tryParseUrl((obj as GraphQL.Document).url as string); | ||
|
||
const schema = | ||
'data' in obj && '__schema' in (obj as GraphQL.Document).data | ||
? (obj as GraphQL.Document).data.__schema | ||
: undefined; | ||
|
||
const hasRequiredProperties = | ||
!!schema && | ||
typeof schema === 'object' && | ||
typeof schema.queryType === 'object' && | ||
typeof schema.queryType.name === 'string' && | ||
Array.isArray(schema.types); | ||
|
||
return hasValidUrl && hasRequiredProperties; | ||
} | ||
|
||
private tryParseUrl(url: string): boolean { | ||
try { | ||
new URL(url); | ||
|
||
return true; | ||
} catch { | ||
// noop | ||
} | ||
|
||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { | ||
type IntrospectionInputType, | ||
type IntrospectionInputTypeRef, | ||
type IntrospectionListTypeRef, | ||
type IntrospectionNamedTypeRef, | ||
type IntrospectionNonNullTypeRef, | ||
type IntrospectionOutputType, | ||
type IntrospectionOutputTypeRef | ||
} from '@har-sdk/core'; | ||
|
||
export class GraphQlTypeRef< | ||
TypeRef extends IntrospectionOutputTypeRef | IntrospectionInputTypeRef, | ||
Type extends TypeRef extends IntrospectionOutputTypeRef | ||
? IntrospectionOutputType | ||
: IntrospectionInputType | ||
> { | ||
public readonly typeRef: IntrospectionNamedTypeRef<Type>; | ||
|
||
get type(): string { | ||
return this.stringify(this.originalTypeRef); | ||
} | ||
|
||
private readonly originalTypeRef: TypeRef; | ||
|
||
constructor(typeRef: TypeRef) { | ||
this.originalTypeRef = typeRef; | ||
this.typeRef = this.unwrap(typeRef); | ||
} | ||
|
||
private unwrap(typeRef: TypeRef): IntrospectionNamedTypeRef<Type> { | ||
if (this.isNonNullTypeRef(typeRef)) { | ||
return this.unwrap(typeRef.ofType); | ||
} | ||
|
||
if (this.isListTypeRef(typeRef)) { | ||
return this.unwrap(typeRef.ofType); | ||
} | ||
|
||
return typeRef as IntrospectionNamedTypeRef<Type>; | ||
} | ||
|
||
private stringify(typeRef: TypeRef): string { | ||
if (this.isNonNullTypeRef(typeRef)) { | ||
return `${this.stringify(typeRef.ofType)}!`; | ||
} | ||
|
||
if (this.isListTypeRef(typeRef)) { | ||
return `[${this.stringify(typeRef.ofType)}]`; | ||
} | ||
|
||
return (typeRef as IntrospectionNamedTypeRef<Type>).name; | ||
} | ||
|
||
private isNonNullTypeRef( | ||
type: object | ||
): type is IntrospectionNonNullTypeRef<TypeRef> { | ||
return ( | ||
'kind' in type && | ||
(type as IntrospectionNonNullTypeRef).kind === 'NON_NULL' | ||
); | ||
} | ||
|
||
private isListTypeRef( | ||
type: object | ||
): type is IntrospectionListTypeRef<TypeRef> { | ||
return 'kind' in type && (type as IntrospectionListTypeRef).kind === 'LIST'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { ConverterOptions } from './ConverterOptions'; | ||
export { Converter } from './Converter'; | ||
export { DefaultConverter } from './DefaultConverter'; |
24 changes: 24 additions & 0 deletions
24
packages/graphql/src/converter/input-samplers/DefaultInputSamplers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { InputSamplers } from './InputSamplers'; | ||
import { InputSampler } from './InputSampler'; | ||
import { InputObjectSampler } from './InputObjectSampler'; | ||
import { EnumSampler } from './EnumSampler'; | ||
import { GraphQLListSampler } from './ListSampler'; | ||
import { NonNullSampler } from './NonNullSampler'; | ||
import { UploadScalarSampler } from './UploadScalarSampler'; | ||
import { type IntrospectionInputTypeRef } from '@har-sdk/core'; | ||
|
||
export class DefaultInputSamplers implements InputSamplers { | ||
constructor( | ||
private readonly inputSamplers: InputSampler[] = [ | ||
new GraphQLListSampler(), | ||
new NonNullSampler(), | ||
new InputObjectSampler(), | ||
new EnumSampler(), | ||
new UploadScalarSampler() | ||
] | ||
) {} | ||
|
||
public find(typeRef: IntrospectionInputTypeRef): InputSampler | undefined { | ||
return this.inputSamplers.find((s) => s.supportsType(typeRef)); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
packages/graphql/src/converter/input-samplers/EnumSampler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { InputSampler, type InputSamplerOptions } from './InputSampler'; | ||
import { | ||
type IntrospectionEnumType, | ||
type IntrospectionEnumValue, | ||
type IntrospectionInputTypeRef, | ||
type IntrospectionNamedTypeRef | ||
} from '@har-sdk/core'; | ||
|
||
export class EnumSampler implements InputSampler { | ||
public supportsType( | ||
typeRef: IntrospectionInputTypeRef | ||
): typeRef is IntrospectionNamedTypeRef<IntrospectionEnumType> { | ||
return 'kind' in typeRef && typeRef.kind === 'ENUM'; | ||
} | ||
|
||
public sample( | ||
typeRef: IntrospectionInputTypeRef, | ||
{ schema }: InputSamplerOptions | ||
): string | undefined { | ||
if (!this.supportsType(typeRef)) { | ||
return undefined; | ||
} | ||
|
||
return schema.types | ||
.filter( | ||
(type): type is IntrospectionEnumType => type.name === typeRef.name | ||
) | ||
.map((type) => { | ||
const [value]: readonly IntrospectionEnumValue[] = type.enumValues; | ||
|
||
return value ? value.name : 'null'; | ||
}) | ||
.join(''); | ||
} | ||
} |
93 changes: 93 additions & 0 deletions
93
packages/graphql/src/converter/input-samplers/InputObjectSampler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { InputSampler, type InputSamplerOptions } from './InputSampler'; | ||
import { GraphQlTypeRef } from '../GraphQlTypeRef'; | ||
import { | ||
type IntrospectionInputTypeRef, | ||
type IntrospectionInputObjectType, | ||
type IntrospectionInputType, | ||
type IntrospectionNamedTypeRef, | ||
type IntrospectionSchema | ||
} from '@har-sdk/core'; | ||
|
||
export class InputObjectSampler implements InputSampler { | ||
public supportsType( | ||
typeRef: IntrospectionInputTypeRef | ||
): typeRef is IntrospectionNamedTypeRef<IntrospectionInputObjectType> { | ||
return 'kind' in typeRef && typeRef.kind === 'INPUT_OBJECT'; | ||
} | ||
|
||
public sample( | ||
typeRef: IntrospectionInputTypeRef, | ||
options: InputSamplerOptions | ||
): string | undefined { | ||
if (!this.supportsType(typeRef)) { | ||
return undefined; | ||
} | ||
|
||
const { schema, visitedTypes } = options; | ||
|
||
const type = this.findType(schema, typeRef); | ||
|
||
if (!type) { | ||
return undefined; | ||
} | ||
|
||
visitedTypes.push(type.name); | ||
|
||
try { | ||
const sample = this.sampleFields(type, options).join(', '); | ||
|
||
return sample ? `{${sample}}` : undefined; | ||
} finally { | ||
visitedTypes.pop(); | ||
} | ||
} | ||
|
||
private sampleFields( | ||
type: IntrospectionInputObjectType, | ||
options: InputSamplerOptions | ||
) { | ||
const { inputSamplers, visitedTypes, pointer } = options; | ||
|
||
return type.inputFields | ||
.map((field) => { | ||
const resolvedTypeRef = new GraphQlTypeRef(field.type); | ||
const { typeRef } = resolvedTypeRef; | ||
|
||
const visited = visitedTypes.includes(typeRef.name); | ||
|
||
if (visited) { | ||
return undefined; | ||
} | ||
|
||
if (field.defaultValue !== undefined && field.defaultValue !== null) { | ||
return field.defaultValue; | ||
} | ||
|
||
try { | ||
pointer.push(field.name); | ||
|
||
const sample = inputSamplers | ||
.find(field.type) | ||
?.sample(field.type, options); | ||
|
||
return sample ? `${field.name}: ${sample}` : sample; | ||
} finally { | ||
pointer.pop(); | ||
} | ||
}) | ||
.filter((field): field is string => !!field); | ||
} | ||
|
||
private findType( | ||
schema: IntrospectionSchema, | ||
typeRef: IntrospectionNamedTypeRef<IntrospectionInputType> | ||
) { | ||
const [introspectionInputObjectType]: IntrospectionInputObjectType[] = | ||
schema.types.filter( | ||
(type): type is IntrospectionInputObjectType => | ||
type.kind === 'INPUT_OBJECT' && type.name === typeRef.name | ||
); | ||
|
||
return introspectionInputObjectType; | ||
} | ||
} |
Oops, something went wrong.