Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All at once with GraphQL Config #711

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion action/index.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@types/express": "4.17.6",
"@types/jest": "25.2.1",
"@types/node": "13.13.4",
"@types/yamljs": "0.2.30",
"@types/yargs": "15.0.4",
"@zeit/ncc": "0.22.1",
"axios": "0.19.2",
Expand All @@ -61,8 +62,7 @@
"graphql-request": "1.8.2",
"husky": "4.2.5",
"immer": "6.0.3",
"lerna": "3.20.2",
"shelljs": "0.8.4"
"lerna": "3.20.2"
},
"workspaces": [
"packages/*",
Expand Down
31 changes: 18 additions & 13 deletions packages/ci/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,30 @@ async function main() {
.help()
.showHelpOnFail(false)
.fail((msg, error) => {
if (msg.includes('Unknown argument:')) {
const commandName = msg.replace('Unknown argument: ', '').toLowerCase();
if (msg) {
if (msg.includes('Unknown argument:')) {
const commandName = msg
.replace('Unknown argument: ', '')
.toLowerCase();

Logger.error(`Command '${commandName}' not found`);
Logger.error(`Command '${commandName}' not found`);

if (availableCommands.includes(commandName)) {
Logger.log(
` Try to install @graphql-inspector/${commandName}-command`,
);
if (availableCommands.includes(commandName)) {
Logger.log(
` Try to install @graphql-inspector/${commandName}-command`,
);
}
} else if (msg.includes('Not enough')) {
Logger.error(msg);
Logger.info('Specify --help for available options');
} else {
Logger.error(msg);
}
} else if (msg.includes('Not enough')) {
Logger.error(msg);
Logger.info('Specify --help for available options');
} else {
Logger.error(msg);
}

if (error) {
throw error;
Logger.error(error.message);
console.error(error);
}

process.exit(1);
Expand Down
1 change: 1 addition & 0 deletions packages/commands/commands/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"yargs": "15.3.1"
},
"dependencies": {
"yamljs": "0.3.0",
"tslib": "^1.11.1"
},
"scripts": {
Expand Down
148 changes: 148 additions & 0 deletions packages/commands/commands/src/graphql-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {GraphQLConfig} from '@graphql-inspector/config';
import {readFile, stat} from 'fs';
import {resolve} from 'path';
import {parse as parseYAML} from 'yamljs';

const searchPlaces = [
`graphql.config.js`,
'graphql.config.json',
'graphql.config.yaml',
'graphql.config.yml',
'.graphqlrc',
'.graphqlrc.js',
'.graphqlrc.json',
'.graphqlrc.yml',
'.graphqlrc.yaml',
];

const legacySearchPlaces = [
'.graphqlconfig',
'.graphqlconfig.json',
'.graphqlconfig.yaml',
'.graphqlconfig.yml',
];

export type PickPointers = (
args: {
config?: boolean;
schema?: string;
documents?: string;
},
required: {
schema: boolean;
documents: boolean;
},
) => Promise<{
schema?: string;
documents?: string;
}>;

export const pickPointers: PickPointers = async (args, required) => {
let schema: string | undefined;
let documents: string | undefined;

if (args.config) {
const config = await useGraphQLConfig();

schema = config.schema;
documents = config.documents;
} else {
schema = args.schema;
documents = args.documents;
}

if (required.documents || required.schema) {
const errors: string[] = [];

if (required.schema && !schema) {
errors.push(`Schema pointer is missing`);
}

if (required.documents && !documents) {
errors.push(`Documents pointer is missing`);
}

if (errors.length) {
throw new Error(errors.join('\n'));
}
}

return {
schema,
documents,
};
};

async function useGraphQLConfig(): Promise<GraphQLConfig> {
const found = await firstExisting(searchPlaces);

if (found) {
return parse(found);
}

const foundLegacy = await firstExisting(legacySearchPlaces);

if (foundLegacy) {
return parse(foundLegacy);
}

throw new Error(`Couldn't find a GraphQL Config`);
}

async function firstExisting(places: string[]) {
const statuses = await Promise.all(
places.map((place) => resolve(process.cwd(), place)).map(exists),
);

return places.find((_, i) => statuses[i] === true);
}

async function parse(path: string): Promise<GraphQLConfig> {
const normalizedPath = resolve(process.cwd(), path.toLowerCase());

if (normalizedPath.endsWith('.js')) {
const mod = require(normalizedPath);

return mod.default || mod;
}

const content = await read(path);

if (normalizedPath.endsWith('.yml') || normalizedPath.endsWith('.yaml')) {
return parseYAML(content);
}

if (normalizedPath.endsWith('.json')) {
return JSON.parse(content);
}

try {
return parseYAML(content);
} catch (error) {}

try {
return JSON.parse(content);
} catch (error) {}

throw new Error(`Failed to parse: ${path}`);
}

function read(path: string): Promise<string> {
return new Promise((resolve, reject) => {
readFile(path, 'utf-8', (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}

function exists(path: string): Promise<boolean> {
return new Promise((resolve) => {
stat(path, (error) => {
resolve(error ? false : true);
});
});
}
13 changes: 11 additions & 2 deletions packages/commands/commands/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,28 @@ import {InspectorConfig} from '@graphql-inspector/config';
import {Loaders} from '@graphql-inspector/loaders';
import {isAbsolute, resolve} from 'path';
import yargs, {CommandModule} from 'yargs';
import {pickPointers, PickPointers} from './graphql-config';

export {CommandModule as Command};
export {CommandModule as Command, PickPointers, pickPointers};

export interface UseCommandsAPI {
config: InspectorConfig;
loaders: Loaders;
pickPointers: PickPointers;
}

export type CommandFactory<T = {}, U = {}> = (
api: UseCommandsAPI,
) => CommandModule<T, U>;

export function useCommands(api: UseCommandsAPI): CommandModule[] {
export function useCommands(
input: Omit<UseCommandsAPI, 'pickPointers'>,
): CommandModule[] {
const api = {
...input,
pickPointers,
};

return api.config.commands.map((name) => loadCommand(name)(api));
}

Expand Down
25 changes: 17 additions & 8 deletions packages/commands/coverage/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,25 @@ import {writeFileSync} from 'fs';
export default createCommand<
{},
{
schema: string;
documents: string;
schema?: string;
documents?: string;
config?: boolean;
write?: string;
silent?: boolean;
} & GlobalArgs
>((api) => {
return {
command: 'coverage <documents> <schema>',
command: 'coverage [documents] [schema]',
describe: 'Schema coverage based on documents',
builder(yargs) {
return yargs
.positional('schema', {
describe: 'Point to a schema',
type: 'string',
demandOption: true,
})
.positional('documents', {
describe: 'Point to documents',
type: 'string',
demandOption: true,
})
.options({
w: {
Expand All @@ -49,19 +48,29 @@ export default createCommand<
describe: 'Do not render any stats in the terminal',
type: 'boolean',
},
c: {
alias: 'config',
describe: 'Use GraphQL Config to find schema and documents',
type: 'boolean',
},
});
},
async handler(args) {
const {loaders} = api;
const {loaders, pickPointers} = api;
const writePath = args.write;
const shouldWrite = typeof writePath !== 'undefined';
const {headers, token} = parseGlobalArgs(args);

const schema = await loaders.loadSchema(args.schema, {
const pointer = await pickPointers(args, {
documents: true,
schema: true,
});

const schema = await loaders.loadSchema(pointer.schema!, {
token,
headers,
});
const documents = await loaders.loadDocuments(args.documents);
const documents = await loaders.loadDocuments(pointer.documents!);
const coverage = calculateCoverage(
schema,
documents.map((doc) => new Source(print(doc.document!), doc.location)),
Expand Down
19 changes: 14 additions & 5 deletions packages/commands/serve/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,20 @@ import {fake} from './fake';
export default createCommand<
{},
{
schema: string;
schema?: string;
port: number;
} & GlobalArgs
>((api) => {
const {loaders} = api;
const {loaders, pickPointers} = api;

return {
command: 'serve <schema>',
command: 'serve [schema]',
describe: 'Compare two GraphQL Schemas',
builder(yargs) {
return yargs
.positional('schema', {
describe: 'Point to a schema',
type: 'string',
demandOption: true,
})
.options({
port: {
Expand All @@ -36,11 +35,21 @@ export default createCommand<
type: 'number',
default: 4000,
},
c: {
alias: 'config',
describe: 'Use GraphQL Config to find schema and documents',
type: 'boolean',
},
});
},
async handler(args) {
const {headers, token} = parseGlobalArgs(args);
const schema = await loaders.loadSchema(args.schema, {
const pointer = await pickPointers(args, {
schema: true,
documents: false,
});

const schema = await loaders.loadSchema(pointer.schema!, {
headers,
token,
});
Expand Down
Loading