-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(graphql): add a script that shows whether a field is used or not
- Loading branch information
Showing
4 changed files
with
384 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
#!/usr/bin/env node | ||
|
||
var path = require('path'); | ||
require(path.resolve(__dirname, '../lib/cli/check-used.js')).run(process.argv); |
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 @@ | ||
export declare function run(argv: string[]): Promise<void>; |
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,183 @@ | ||
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.run = void 0; | ||
const promises_1 = __importDefault(require("fs/promises")); | ||
const graphql_1 = require("graphql"); | ||
const common_1 = require("./common"); | ||
function isDeprecated(directives) { | ||
for (const directive of directives || []) { | ||
if (directive.name.value === 'deprecated') { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
async function check(schema_file, query_list_file) { | ||
const schema = (0, graphql_1.buildSchema)(await promises_1.default.readFile(schema_file, 'utf-8')); | ||
const query_list = (await promises_1.default.readFile(query_list_file, 'utf-8')).split('\n'); | ||
for (const query of query_list) { | ||
if (!query) { | ||
continue; | ||
} | ||
const parsed_query = (0, graphql_1.parse)(query); | ||
const type_info = new graphql_1.TypeInfo(schema); | ||
const visitor = { | ||
enter(node) { | ||
if (node.kind === graphql_1.Kind.NAMED_TYPE) { | ||
const name = node.name.value; | ||
const type = schema.getType(name); | ||
if (type) { | ||
type.extensions = { | ||
used: true, | ||
query_list: [...(type.extensions.query_list || []), query], | ||
}; | ||
} | ||
} | ||
if (node.kind === graphql_1.Kind.FIELD) { | ||
const field = type_info.getFieldDef(); | ||
if (field) { | ||
field.extensions = { | ||
used: true, | ||
query_list: [...(field.extensions.query_list || []), query], | ||
}; | ||
if (node.arguments) { | ||
for (const argument of node.arguments) { | ||
const field_arg = field.args.find((arg) => arg.name === argument.name.value); | ||
if (field_arg) { | ||
field_arg.extensions = { | ||
used: true, | ||
query_list: [...(field_arg.extensions.query_list || []), query], | ||
}; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
}; | ||
(0, graphql_1.visit)(parsed_query, (0, graphql_1.visitWithTypeInfo)(type_info, visitor)); | ||
} | ||
return schema; | ||
} | ||
function showAll(schema) { | ||
const types = Object.values(schema.getTypeMap()); | ||
for (const type of types) { | ||
if (type.name.startsWith('__')) { | ||
continue; | ||
} | ||
if ((0, graphql_1.isInputObjectType)(type) || (0, graphql_1.isObjectType)(type)) { | ||
showType(type); | ||
} | ||
} | ||
} | ||
function showType(type) { | ||
const type_str = (0, graphql_1.isInputObjectType)(type) ? 'input' : 'type'; | ||
const has_used_field = Object.values(type.getFields()).some((field) => field.extensions.used); | ||
const is_type_deprecated = isDeprecated(type.astNode?.directives); | ||
let type_status = ''; | ||
if (type.extensions.used) { | ||
type_status = is_type_deprecated | ||
? `${common_1.COLORS.RED}used(deprecated)${common_1.COLORS.RESET}` | ||
: `${common_1.COLORS.GREEN}used${common_1.COLORS.RESET}`; | ||
} | ||
else { | ||
if (has_used_field) { | ||
type_status = is_type_deprecated | ||
? `${common_1.COLORS.RED}no reference(deprecated)${common_1.COLORS.RESET}` | ||
: `${common_1.COLORS.BLUE}no reference${common_1.COLORS.RESET}`; | ||
} | ||
else { | ||
type_status = is_type_deprecated | ||
? `${common_1.COLORS.RED}never(deprecated)${common_1.COLORS.RESET}` | ||
: `${common_1.COLORS.RED}never${common_1.COLORS.RESET}`; | ||
} | ||
} | ||
console.log(`${type_str} ${type.name}: ${type_status}`); | ||
for (const field of Object.values(type.getFields())) { | ||
const is_field_deprecated = isDeprecated(field.astNode?.directives); | ||
let field_status = ''; | ||
if (field.extensions.used) { | ||
field_status = is_field_deprecated | ||
? `${common_1.COLORS.RED}used(deprecated)${common_1.COLORS.RESET}` | ||
: `${common_1.COLORS.GREEN}used${common_1.COLORS.RESET}`; | ||
} | ||
else { | ||
field_status = is_field_deprecated | ||
? `${common_1.COLORS.RED}never(deprecated)${common_1.COLORS.RESET}` | ||
: `${common_1.COLORS.RED}never${common_1.COLORS.RESET}`; | ||
} | ||
console.log(` ${type.name}.${field.name}: ${field_status}`); | ||
if ('args' in field) { | ||
for (const arg of field.args) { | ||
const is_arg_deprecated = isDeprecated(arg.astNode?.directives); | ||
let arg_status = ''; | ||
if (arg.extensions.used) { | ||
arg_status = is_arg_deprecated | ||
? `${common_1.COLORS.RED}used(deprecated)${common_1.COLORS.RESET}` | ||
: `${common_1.COLORS.GREEN}used${common_1.COLORS.RESET}`; | ||
} | ||
else { | ||
arg_status = is_arg_deprecated | ||
? `${common_1.COLORS.RED}never(deprecated)${common_1.COLORS.RESET}` | ||
: `${common_1.COLORS.RED}never${common_1.COLORS.RESET}`; | ||
} | ||
console.log(` (${arg.name}): ${arg_status}`); | ||
} | ||
} | ||
} | ||
} | ||
function showOfTypeOrField(schema, type_or_field) { | ||
const types = Object.values(schema.getTypeMap()); | ||
for (const type of types) { | ||
if (!((0, graphql_1.isInputObjectType)(type) || (0, graphql_1.isObjectType)(type))) { | ||
continue; | ||
} | ||
if (type.name === type_or_field) { | ||
showType(type); | ||
const query_list = []; | ||
for (const field of Object.values(type.getFields())) { | ||
query_list.push(...(field.extensions.query_list || [])); | ||
} | ||
showQueryList(query_list); | ||
} | ||
if (type_or_field.startsWith(`${type.name}.`)) { | ||
showType(type); | ||
const field_name = type_or_field.split('.')[1]; | ||
const field = type.getFields()[field_name]; | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
if (field) { | ||
const query_list = field.extensions.query_list || []; | ||
showQueryList(query_list); | ||
} | ||
} | ||
} | ||
} | ||
function showQueryList(query_list) { | ||
const unique_query_list = [...new Set(query_list)]; | ||
console.log(`Used by ${unique_query_list.length} queries`); | ||
for (const query of unique_query_list) { | ||
showQuery(query); | ||
} | ||
} | ||
function showQuery(query) { | ||
console.log('-'.repeat(120)); | ||
console.log((0, graphql_1.print)((0, graphql_1.parse)(query))); | ||
} | ||
async function run(argv) { | ||
if (argv.length < 4) { | ||
throw new Error('Usage: check-used <schema file> <query list> [type or field]'); | ||
} | ||
const schema_file = argv[2]; | ||
const query_list_file = argv[3]; | ||
const schema = await check(schema_file, query_list_file); | ||
if (argv.length === 4) { | ||
showAll(schema); | ||
} | ||
else { | ||
showOfTypeOrField(schema, argv[4]); | ||
} | ||
} | ||
exports.run = run; |
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,196 @@ | ||
import fs from 'fs/promises'; | ||
import { | ||
ASTVisitor, | ||
ConstDirectiveNode, | ||
GraphQLInputObjectType, | ||
GraphQLObjectType, | ||
GraphQLSchema, | ||
Kind, | ||
TypeInfo, | ||
buildSchema, | ||
isInputObjectType, | ||
isObjectType, | ||
parse, | ||
print, | ||
visit, | ||
visitWithTypeInfo, | ||
} from 'graphql'; | ||
import { COLORS } from './common'; | ||
|
||
function isDeprecated(directives: readonly ConstDirectiveNode[] | undefined) { | ||
for (const directive of directives || []) { | ||
if (directive.name.value === 'deprecated') { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
async function check(schema_file: string, query_list_file: string) { | ||
const schema = buildSchema(await fs.readFile(schema_file, 'utf-8')); | ||
const query_list = (await fs.readFile(query_list_file, 'utf-8')).split('\n'); | ||
for (const query of query_list) { | ||
if (!query) { | ||
continue; | ||
} | ||
const parsed_query = parse(query); | ||
const type_info = new TypeInfo(schema); | ||
const visitor: ASTVisitor = { | ||
enter(node) { | ||
if (node.kind === Kind.NAMED_TYPE) { | ||
const name = node.name.value; | ||
const type = schema.getType(name); | ||
if (type) { | ||
type.extensions = { | ||
used: true, | ||
query_list: [...((type.extensions.query_list as string[] | undefined) || []), query], | ||
}; | ||
} | ||
} | ||
if (node.kind === Kind.FIELD) { | ||
const field = type_info.getFieldDef(); | ||
if (field) { | ||
field.extensions = { | ||
used: true, | ||
query_list: [...((field.extensions.query_list as string[] | undefined) || []), query], | ||
}; | ||
if (node.arguments) { | ||
for (const argument of node.arguments) { | ||
const field_arg = field.args.find((arg) => arg.name === argument.name.value); | ||
if (field_arg) { | ||
field_arg.extensions = { | ||
used: true, | ||
query_list: [...((field_arg.extensions.query_list as string[] | undefined) || []), query], | ||
}; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
}; | ||
visit(parsed_query, visitWithTypeInfo(type_info, visitor)); | ||
} | ||
return schema; | ||
} | ||
|
||
function showAll(schema: GraphQLSchema) { | ||
const types = Object.values(schema.getTypeMap()); | ||
for (const type of types) { | ||
if (type.name.startsWith('__')) { | ||
continue; | ||
} | ||
if (isInputObjectType(type) || isObjectType(type)) { | ||
showType(type); | ||
} | ||
} | ||
} | ||
|
||
function showType(type: GraphQLInputObjectType | GraphQLObjectType) { | ||
const type_str = isInputObjectType(type) ? 'input' : 'type'; | ||
const has_used_field = Object.values(type.getFields()).some((field) => field.extensions.used); | ||
const is_type_deprecated = isDeprecated(type.astNode?.directives); | ||
let type_status = ''; | ||
if (type.extensions.used) { | ||
type_status = is_type_deprecated | ||
? `${COLORS.RED}used(deprecated)${COLORS.RESET}` | ||
: `${COLORS.GREEN}used${COLORS.RESET}`; | ||
} else { | ||
if (has_used_field) { | ||
type_status = is_type_deprecated | ||
? `${COLORS.RED}no reference(deprecated)${COLORS.RESET}` | ||
: `${COLORS.BLUE}no reference${COLORS.RESET}`; | ||
} else { | ||
type_status = is_type_deprecated | ||
? `${COLORS.RED}never(deprecated)${COLORS.RESET}` | ||
: `${COLORS.RED}never${COLORS.RESET}`; | ||
} | ||
} | ||
console.log(`${type_str} ${type.name}: ${type_status}`); | ||
for (const field of Object.values(type.getFields())) { | ||
const is_field_deprecated = isDeprecated(field.astNode?.directives); | ||
let field_status = ''; | ||
if (field.extensions.used) { | ||
field_status = is_field_deprecated | ||
? `${COLORS.RED}used(deprecated)${COLORS.RESET}` | ||
: `${COLORS.GREEN}used${COLORS.RESET}`; | ||
} else { | ||
field_status = is_field_deprecated | ||
? `${COLORS.RED}never(deprecated)${COLORS.RESET}` | ||
: `${COLORS.RED}never${COLORS.RESET}`; | ||
} | ||
console.log(` ${type.name}.${field.name}: ${field_status}`); | ||
if ('args' in field) { | ||
for (const arg of field.args) { | ||
const is_arg_deprecated = isDeprecated(arg.astNode?.directives); | ||
let arg_status = ''; | ||
if (arg.extensions.used) { | ||
arg_status = is_arg_deprecated | ||
? `${COLORS.RED}used(deprecated)${COLORS.RESET}` | ||
: `${COLORS.GREEN}used${COLORS.RESET}`; | ||
} else { | ||
arg_status = is_arg_deprecated | ||
? `${COLORS.RED}never(deprecated)${COLORS.RESET}` | ||
: `${COLORS.RED}never${COLORS.RESET}`; | ||
} | ||
console.log(` (${arg.name}): ${arg_status}`); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function showOfTypeOrField(schema: GraphQLSchema, type_or_field: string) { | ||
const types = Object.values(schema.getTypeMap()); | ||
for (const type of types) { | ||
if (!(isInputObjectType(type) || isObjectType(type))) { | ||
continue; | ||
} | ||
if (type.name === type_or_field) { | ||
showType(type); | ||
const query_list: string[] = []; | ||
for (const field of Object.values(type.getFields())) { | ||
query_list.push(...((field.extensions.query_list as string[] | undefined) || [])); | ||
} | ||
showQueryList(query_list); | ||
} | ||
if (type_or_field.startsWith(`${type.name}.`)) { | ||
showType(type); | ||
const field_name = type_or_field.split('.')[1]; | ||
const field = type.getFields()[field_name]; | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
if (field) { | ||
const query_list = (field.extensions.query_list as string[] | undefined) || []; | ||
showQueryList(query_list); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function showQueryList(query_list: string[]) { | ||
const unique_query_list = [...new Set(query_list)]; | ||
console.log(`Used by ${unique_query_list.length} queries`); | ||
for (const query of unique_query_list) { | ||
showQuery(query); | ||
} | ||
} | ||
|
||
function showQuery(query: string) { | ||
console.log('-'.repeat(120)); | ||
console.log(print(parse(query))); | ||
} | ||
|
||
export async function run(argv: string[]) { | ||
if (argv.length < 4) { | ||
throw new Error('Usage: check-used <schema file> <query list> [type or field]'); | ||
} | ||
const schema_file = argv[2]; | ||
const query_list_file = argv[3]; | ||
|
||
const schema = await check(schema_file, query_list_file); | ||
|
||
if (argv.length === 4) { | ||
showAll(schema); | ||
} else { | ||
showOfTypeOrField(schema, argv[4]); | ||
} | ||
} |