From 5739a25a51128a4360a8007fcebc9446a0c64a48 Mon Sep 17 00:00:00 2001 From: timsuchanek Date: Tue, 22 May 2018 15:40:29 +0200 Subject: [PATCH] fix: exclude root types from wildcard import --- package.json | 33 ++++++------- src/index.test.ts | 97 +++++++++++++++++++++++++++----------- src/index.ts | 115 +++++++++++++++++++++++++++++++--------------- 3 files changed, 163 insertions(+), 82 deletions(-) diff --git a/package.json b/package.json index b1c755b..32e9f56 100644 --- a/package.json +++ b/package.json @@ -6,28 +6,17 @@ }, "license": "MIT", "repository": "git@github.com:graphcool/graphql-import.git", - "files": [ - "dist" - ], + "files": ["dist"], "main": "dist/index.js", "typings": "dist/index.d.ts", "typescript": { "definition": "dist/index.d.ts" }, "nyc": { - "extension": [ - ".ts" - ], - "require": [ - "ts-node/register" - ], - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "**/*.d.ts", - "**/*.test.ts" - ], + "extension": [".ts"], + "require": ["ts-node/register"], + "include": ["src/**/*.ts"], + "exclude": ["**/*.d.ts", "**/*.test.ts"], "all": true, "sourceMap": true, "instrument": true @@ -35,11 +24,15 @@ "scripts": { "prepare": "npm run build", "build": "rm -rf dist && tsc -d", - "testlocal": "npm run build && nyc --reporter lcov --reporter text ava-ts --verbose src/**/*.test.ts", - "test-only": "npm run build && nyc --reporter lcov ava-ts --verbose src/**/*.test.ts --tap | tap-xunit > ~/reports/ava.xml", + "testlocal": + "npm run build && nyc --reporter lcov --reporter text ava-ts -u --verbose src/**/*.test.ts", + "test-only": + "npm run build && mkdir -p ~/reports && nyc --reporter lcov ava-ts --verbose src/**/*.test.ts --tap | tap-xunit > ~/reports/ava.xml", "test": "tslint src/**/*.ts && npm run test-only", - "docs": "typedoc --out docs src/index.ts --hideGenerator --exclude **/*.test.ts", - "docs:publish": "cp ./now.json ./docs && cd docs && now --public -f && now alias && now rm --yes --safe graphql-import & cd .." + "docs": + "typedoc --out docs src/index.ts --hideGenerator --exclude **/*.test.ts", + "docs:publish": + "cp ./now.json ./docs && cd docs && now --public -f && now alias && now rm --yes --safe graphql-import & cd .." }, "peerDependencies": { "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0" diff --git a/src/index.test.ts b/src/index.test.ts index f273c3c..f477cf6 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -183,7 +183,9 @@ test('importSchema: import all from objects', t => { }` const schemas = { - schemaA, schemaB, schemaC + schemaA, + schemaB, + schemaC, } const expectedSDL = `\ @@ -246,7 +248,7 @@ test(`importSchema: import all mix 'n match`, t => { }` const schemas = { - schemaB + schemaB, } const expectedSDL = `\ @@ -274,7 +276,6 @@ type C2 { }) test(`importSchema: import all mix 'n match 2`, t => { - const schemaA = ` # import * from "fixtures/import-all/b.graphql" @@ -360,7 +361,9 @@ test(`importSchema: import all - exclude Query/Mutation/Subscription type`, t => }` const schemas = { - schemaA, schemaB, schemaC + schemaA, + schemaB, + schemaC, } const expectedSDL = `\ @@ -499,7 +502,10 @@ type B2 implements B { id: ID! } ` - t.is(importSchema('fixtures/interfaces-implements-many/a.graphql'), expectedSDL) + t.is( + importSchema('fixtures/interfaces-implements-many/a.graphql'), + expectedSDL, + ) }) test('importSchema: input types', t => { @@ -602,14 +608,14 @@ interface Node { test('root field imports', t => { const expectedSDL = `\ -type Dummy { - field: String -} - type Query { posts(filter: PostFilter): [Post] } +type Dummy { + field: String +} + type Post { field1: String } @@ -624,16 +630,16 @@ input PostFilter { test('merged root field imports', t => { const expectedSDL = `\ -type Dummy { - field: String -} - type Query { helloA: String posts(filter: PostFilter): [Post] hello: String } +type Dummy { + field: String +} + type Post { field1: String } @@ -666,36 +672,75 @@ type Shared { }) test('missing type on type', t => { - const err = t.throws(() => importSchema('fixtures/type-not-found/a.graphql'), Error) - t.is(err.message, `Field test: Couldn't find type Post in any of the schemas.`) + const err = t.throws( + () => importSchema('fixtures/type-not-found/a.graphql'), + Error, + ) + t.is( + err.message, + `Field test: Couldn't find type Post in any of the schemas.`, + ) }) test('missing type on interface', t => { - const err = t.throws(() => importSchema('fixtures/type-not-found/b.graphql'), Error) - t.is(err.message, `Field test: Couldn't find type Post in any of the schemas.`) + const err = t.throws( + () => importSchema('fixtures/type-not-found/b.graphql'), + Error, + ) + t.is( + err.message, + `Field test: Couldn't find type Post in any of the schemas.`, + ) }) test('missing type on input type', t => { - const err = t.throws(() => importSchema('fixtures/type-not-found/c.graphql'), Error) - t.is(err.message, `Field post: Couldn't find type Post in any of the schemas.`) + const err = t.throws( + () => importSchema('fixtures/type-not-found/c.graphql'), + Error, + ) + t.is( + err.message, + `Field post: Couldn't find type Post in any of the schemas.`, + ) }) test('missing interface type', t => { - const err = t.throws(() => importSchema('fixtures/type-not-found/d.graphql'), Error) - t.is(err.message, `Couldn't find interface MyInterface in any of the schemas.`) + const err = t.throws( + () => importSchema('fixtures/type-not-found/d.graphql'), + Error, + ) + t.is( + err.message, + `Couldn't find interface MyInterface in any of the schemas.`, + ) }) test('missing union type', t => { - const err = t.throws(() => importSchema('fixtures/type-not-found/e.graphql'), Error) + const err = t.throws( + () => importSchema('fixtures/type-not-found/e.graphql'), + Error, + ) t.is(err.message, `Couldn't find type C in any of the schemas.`) }) test('missing type on input type', t => { - const err = t.throws(() => importSchema('fixtures/type-not-found/f.graphql'), Error) - t.is(err.message, `Field myfield: Couldn't find type Post in any of the schemas.`) + const err = t.throws( + () => importSchema('fixtures/type-not-found/f.graphql'), + Error, + ) + t.is( + err.message, + `Field myfield: Couldn't find type Post in any of the schemas.`, + ) }) test('missing type on directive', t => { - const err = t.throws(() => importSchema('fixtures/type-not-found/g.graphql'), Error) - t.is(err.message, `Directive first: Couldn't find type first in any of the schemas.`) + const err = t.throws( + () => importSchema('fixtures/type-not-found/g.graphql'), + Error, + ) + t.is( + err.message, + `Directive first: Couldn't find type first in any of the schemas.`, + ) }) diff --git a/src/index.ts b/src/index.ts index e3a4f91..197dcce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,18 @@ import * as fs from 'fs' -import { DefinitionNode, parse, print, TypeDefinitionNode, GraphQLObjectType, ObjectTypeDefinitionNode, DocumentNode, Kind } from 'graphql' -import { flatten, groupBy, includes } from 'lodash' +import { + DefinitionNode, + parse, + print, + TypeDefinitionNode, + GraphQLObjectType, + ObjectTypeDefinitionNode, + DocumentNode, + Kind, +} from 'graphql' +import { flatten, groupBy, includes, keyBy } from 'lodash' import * as path from 'path' -import { - completeDefinitionPool, - ValidDefinitionNode, -} from './definition' +import { completeDefinitionPool, ValidDefinitionNode } from './definition' /** * Describes the information from a single import line @@ -17,6 +23,8 @@ export interface RawModule { from: string } +const rootFields = ['Query', 'Mutation', 'Subscription'] + const read = (schema: string, schemas?: { [key: string]: string }) => { if (isFile(schema)) { return fs.readFileSync(schema, { encoding: 'utf8' }) @@ -71,7 +79,10 @@ export function parseSDL(sdl: string): RawModule[] { * @param filePath File path to the initial schema file * @returns Single bundled schema with all imported types */ -export function importSchema(schema: string, schemas?: { [key: string]: string }): string { +export function importSchema( + schema: string, + schemas?: { [key: string]: string }, +): string { const sdl = read(schema, schemas) || schema const document = getDocumentFromSDL(sdl) @@ -80,17 +91,20 @@ export function importSchema(schema: string, schemas?: { [key: string]: string } ['*'], sdl, schema, - schemas + schemas, ) // Post processing of the final schema (missing types, unused types, etc.) // Query, Mutation and Subscription should be merged // And should always be in the first set, to make sure they // are not filtered out. - const typesToFilter = ['Query', 'Mutation', 'Subscription'] - const firstTypes = flatten(typeDefinitions).filter(d => includes(typesToFilter, d.name.value)) - const otherFirstTypes = typeDefinitions[0].filter(d => !includes(typesToFilter, d.name.value)) - const firstSet = otherFirstTypes.concat(firstTypes) + const firstTypes = flatten(typeDefinitions).filter(d => + includes(rootFields, d.name.value), + ) + const otherFirstTypes = typeDefinitions[0].filter( + d => !includes(rootFields, d.name.value), + ) + const firstSet = firstTypes.concat(otherFirstTypes) const processedTypeNames = [] const mergedFirstTypes = [] for (const type of firstSet) { @@ -98,8 +112,12 @@ export function importSchema(schema: string, schemas?: { [key: string]: string } processedTypeNames.push(type.name.value) mergedFirstTypes.push(type) } else { - const existingType = mergedFirstTypes.find(t => t.name.value === type.name.value) - existingType.fields = existingType.fields.concat((type as ObjectTypeDefinitionNode).fields) + const existingType = mergedFirstTypes.find( + t => t.name.value === type.name.value, + ) + existingType.fields = existingType.fields.concat( + (type as ObjectTypeDefinitionNode).fields, + ) } } @@ -138,11 +156,12 @@ function getDocumentFromSDL(sdl: string): DocumentNode { * @returns True if SDL only contains comments and/or whitespaces */ function isEmptySDL(sdl: string): boolean { - return sdl - .split('\n') - .map(l => l.trim()) - .filter(l => !(l.length === 0 || l.startsWith('#'))) - .length === 0 + return ( + sdl + .split('\n') + .map(l => l.trim()) + .filter(l => !(l.length === 0 || l.startsWith('#'))).length === 0 + ) } /** @@ -165,7 +184,7 @@ function collectDefinitions( schemas?: { [key: string]: string }, processedFiles: Set = new Set(), typeDefinitions: ValidDefinitionNode[][] = [], - allDefinitions: ValidDefinitionNode[][] = [] + allDefinitions: ValidDefinitionNode[][] = [], ): { allDefinitions: ValidDefinitionNode[][] typeDefinitions: ValidDefinitionNode[][] @@ -182,7 +201,8 @@ function collectDefinitions( // Filter TypeDefinitionNodes by type and defined imports const currentTypeDefinitions = filterImportedDefinitions( imports, - document.definitions + document.definitions, + allDefinitions, ) // Add typedefinitions to running total @@ -208,9 +228,10 @@ function collectDefinitions( // Process each file (recursively) mergedModules.forEach(m => { // If it was not yet processed (in case of circular dependencies) - const moduleFilePath = isFile(filePath) && isFile(m.from) - ? path.resolve(path.join(dirname, m.from)) - : m.from + const moduleFilePath = + isFile(filePath) && isFile(m.from) + ? path.resolve(path.join(dirname, m.from)) + : m.from if (!processedFiles.has(moduleFilePath)) { collectDefinitions( m.imports, @@ -219,7 +240,7 @@ function collectDefinitions( schemas, processedFiles, typeDefinitions, - allDefinitions + allDefinitions, ) } }) @@ -238,26 +259,48 @@ function collectDefinitions( */ function filterImportedDefinitions( imports: string[], - typeDefinitions: DefinitionNode[] + typeDefinitions: DefinitionNode[], + allDefinitions: ValidDefinitionNode[][] = [], ): ValidDefinitionNode[] { - // This should do something smart with fields const filteredDefinitions = filterTypeDefinitions(typeDefinitions) if (includes(imports, '*')) { + if ( + imports.length === 1 && + imports[0] === '*' && + allDefinitions.length > 1 + ) { + const previousTypeDefinitions: { [key: string]: DefinitionNode } = keyBy( + flatten(allDefinitions.slice(0, allDefinitions.length - 1)).filter( + def => !rootFields.includes(def.name.value), + ), + def => def.name.value, + ) + return typeDefinitions.filter( + typeDef => + typeDef.kind === 'ObjectTypeDefinition' && + previousTypeDefinitions[typeDef.name.value], + ) as ObjectTypeDefinitionNode[] + } return filteredDefinitions } else { - const result = filteredDefinitions.filter(d => includes(imports.map(i => i.split('.')[0]), d.name.value)) - const fieldImports = imports - .filter(i => i.split('.').length > 1) + const result = filteredDefinitions.filter(d => + includes(imports.map(i => i.split('.')[0]), d.name.value), + ) + const fieldImports = imports.filter(i => i.split('.').length > 1) const groupedFieldImports = groupBy(fieldImports, x => x.split('.')[0]) for (const rootType in groupedFieldImports) { - const fields = groupedFieldImports[rootType].map(x => x.split('.')[1]); - (filteredDefinitions.find(def => def.name.value === rootType) as ObjectTypeDefinitionNode).fields = - (filteredDefinitions.find(def => def.name.value === rootType) as ObjectTypeDefinitionNode).fields - .filter(f => includes(fields, f.name.value) || includes(fields, '*')) + const fields = groupedFieldImports[rootType].map(x => x.split('.')[1]) + ;(filteredDefinitions.find( + def => def.name.value === rootType, + ) as ObjectTypeDefinitionNode).fields = (filteredDefinitions.find( + def => def.name.value === rootType, + ) as ObjectTypeDefinitionNode).fields.filter( + f => includes(fields, f.name.value) || includes(fields, '*'), + ) } return result @@ -271,7 +314,7 @@ function filterImportedDefinitions( * @returns Relevant type definitions */ function filterTypeDefinitions( - definitions: DefinitionNode[] + definitions: DefinitionNode[], ): ValidDefinitionNode[] { const validKinds = [ 'DirectiveDefinition', @@ -280,7 +323,7 @@ function filterTypeDefinitions( 'InterfaceTypeDefinition', 'EnumTypeDefinition', 'UnionTypeDefinition', - 'InputObjectTypeDefinition' + 'InputObjectTypeDefinition', ] return definitions .filter(d => includes(validKinds, d.kind))