-
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(core): introduce GraphQL importer
closes #232
- Loading branch information
Showing
15 changed files
with
3,809 additions
and
30 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
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,104 @@ | ||
import { BaseImporter } from './BaseImporter'; | ||
import { ImporterType } from './ImporterType'; | ||
import { isArrayOfStrings } from '../utils'; | ||
import { type DocFormat, type Spec } from './Spec'; | ||
import { GraphQL, introspectionFromSchema } from '../types'; | ||
import { loadSchema } from '@graphql-tools/load'; | ||
import { URL } from 'url'; | ||
import { type BinaryLike, createHash } from 'crypto'; | ||
|
||
export class GraphQLImporter extends BaseImporter<ImporterType.GRAPHQL> { | ||
get type(): ImporterType.GRAPHQL { | ||
return ImporterType.GRAPHQL; | ||
} | ||
|
||
constructor() { | ||
super(); | ||
} | ||
|
||
public async import( | ||
content: string, | ||
expectedFormat?: DocFormat | ||
): Promise<Spec<ImporterType.GRAPHQL, GraphQL.Document> | undefined> { | ||
try { | ||
const spec = await super.import(content, expectedFormat); | ||
|
||
return spec | ||
? { | ||
...spec, | ||
doc: await this.tryConvertSDL(spec.doc) | ||
} | ||
: spec; | ||
} catch { | ||
// noop | ||
} | ||
|
||
return Promise.resolve(undefined); | ||
} | ||
|
||
public isSupported(spec: unknown): spec is GraphQL.Document { | ||
return ( | ||
this.isGraphQLSDLEnvelope(spec) || | ||
this.isGraphQlIntrospectionEnvelope(spec) | ||
); | ||
} | ||
|
||
protected fileName({ | ||
doc | ||
}: { | ||
doc: GraphQL.Document; | ||
format: DocFormat; | ||
}): string | undefined { | ||
const url = new URL(doc.url); | ||
const checkSum = this.generateCheckSum(url.toString()); | ||
|
||
return `${url.hostname}-${checkSum}`.toLowerCase(); | ||
} | ||
|
||
private async tryConvertSDL( | ||
obj: GraphQL.Document | ||
): Promise<GraphQL.Document> { | ||
if (this.isGraphQLSDLEnvelope(obj)) { | ||
const schema = await loadSchema(obj.data, { | ||
loaders: [] | ||
}); | ||
|
||
return { | ||
...obj, | ||
data: introspectionFromSchema(schema) | ||
}; | ||
} | ||
|
||
return obj; | ||
} | ||
|
||
private isGraphQLSDLEnvelope( | ||
obj: unknown | ||
): obj is GraphQL.GraphQLEnvelope<string | string[]> { | ||
return ( | ||
typeof obj === 'object' && | ||
'url' in obj && | ||
typeof (obj as GraphQL.GraphQLEnvelope<string>).url === 'string' && | ||
'data' in obj && | ||
(typeof (obj as GraphQL.GraphQLEnvelope<string>).data === 'string' || | ||
isArrayOfStrings((obj as GraphQL.GraphQLEnvelope<string[]>).data)) | ||
); | ||
} | ||
|
||
private isGraphQlIntrospectionEnvelope( | ||
obj: unknown | ||
): obj is GraphQL.Document { | ||
return ( | ||
typeof obj === 'object' && | ||
'url' in obj && | ||
typeof (obj as GraphQL.Document).url === 'string' && | ||
'data' in obj && | ||
'__schema' in (obj as GraphQL.Document).data && | ||
typeof (obj as GraphQL.Document).data.__schema === 'object' | ||
); | ||
} | ||
|
||
private generateCheckSum(value: BinaryLike): string { | ||
return createHash('md5').update(value).digest('hex'); | ||
} | ||
} |
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
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,15 @@ | ||
import { IntrospectionQuery } from 'graphql'; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-namespace | ||
export namespace GraphQL { | ||
export interface GraphQLEnvelope< | ||
T extends IntrospectionQuery | string | string[] | ||
> { | ||
url: string; | ||
data: T; | ||
} | ||
|
||
export type Document = GraphQLEnvelope<IntrospectionQuery>; | ||
} | ||
|
||
export * from 'graphql'; |
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 |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './har'; | ||
export * from './openapi'; | ||
export * from './postman'; | ||
export * from './openapi'; | ||
export * from './graphql'; |
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 |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './first'; | ||
export * from './url'; | ||
export * from './is-array-of-strings' |
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 const isArrayOfStrings = (data: unknown) => !!data && Array.isArray(data) | ||
? data.every(item => typeof item === 'string') | ||
: 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,95 @@ | ||
import { GraphQLImporter } from '../src/importers/GraphQLImporter'; | ||
import { readFile } from 'fs'; | ||
import { resolve } from 'path'; | ||
import { promisify } from 'util'; | ||
|
||
describe('GraphQLImporter', () => { | ||
const readFileAsync = promisify(readFile); | ||
|
||
let sut!: GraphQLImporter; | ||
|
||
beforeEach(() => { | ||
sut = new GraphQLImporter(); | ||
}); | ||
|
||
describe('type', () => { | ||
it(`should return graphql`, () => { | ||
// act | ||
const result = sut.type; | ||
|
||
// assert | ||
expect(result).toStrictEqual('graphql'); | ||
}); | ||
}); | ||
|
||
describe('import', () => { | ||
it('should not import unparsable content ', async () => { | ||
// arrange | ||
const content = '{'; | ||
|
||
// act | ||
const spec = await sut.import(content); | ||
|
||
// assert | ||
expect(spec).toBeUndefined(); | ||
}); | ||
|
||
it('should import introspection envelope', async () => { | ||
// arrange | ||
const input = | ||
await promisify(readFile)( | ||
resolve(__dirname, './fixtures/graphql.json'), | ||
'utf8' | ||
); | ||
|
||
const expected = JSON.parse( | ||
await promisify(readFile)( | ||
resolve(__dirname, './fixtures/graphql.result.json'), | ||
'utf8' | ||
) | ||
); | ||
|
||
// act | ||
const spec = await sut.import(input); | ||
|
||
// assert | ||
expect(spec).toMatchObject({ | ||
doc: expected, | ||
format: 'json', | ||
type: 'graphql', | ||
name: 'example.com-c00f7d6a02b8e2fb143fd737b7302c15' | ||
}); | ||
}); | ||
|
||
it('should import SDL envelope', async () => { | ||
// arrange | ||
const input = JSON.stringify({ | ||
url: 'https://example.com/graphql', | ||
data: [ | ||
await readFileAsync( | ||
resolve(__dirname, './fixtures/graphql.graphql'), | ||
'utf-8' | ||
) | ||
] | ||
}); | ||
|
||
const expected = JSON.parse( | ||
await promisify(readFile)( | ||
resolve(__dirname, './fixtures/graphql.result.json'), | ||
'utf8' | ||
) | ||
); | ||
|
||
// act | ||
const spec = await sut.import(input); | ||
|
||
// assert | ||
expect(spec).toMatchObject({ | ||
doc: expected, | ||
format: 'json', | ||
type: 'graphql', | ||
name: 'example.com-c00f7d6a02b8e2fb143fd737b7302c15' | ||
}); | ||
}); | ||
}); | ||
}); |
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,43 @@ | ||
interface Identifiable { | ||
id: ID! | ||
} | ||
|
||
type Foo implements Identifiable { | ||
id: ID! | ||
fooField: String! | ||
} | ||
|
||
type Bar implements Identifiable { | ||
id: ID! | ||
barField: Int! | ||
} | ||
|
||
type Baz { | ||
id: ID! | ||
bazField: Float! | ||
} | ||
|
||
type Qux { | ||
id: ID! | ||
quxField: Boolean! | ||
} | ||
|
||
input QuxInput { | ||
quxField: Boolean! | ||
} | ||
|
||
union FooQux = Foo | Qux | ||
|
||
type Query { | ||
getFoo(id: ID!): Foo | ||
getBar(id: ID!): Bar | ||
getBaz(id: ID!): Baz | ||
getFooOrQux(id: ID!): FooQux | ||
} | ||
|
||
type Mutation { | ||
createFoo(fooField: String!): Foo | ||
updateBar(id: ID!, barField: Int!): Bar | ||
deleteBaz(id: ID!): Baz | ||
createQux(qux: QuxInput!): Qux | ||
} |
Oops, something went wrong.