Skip to content

Commit

Permalink
Refactor/1.1 refacto (#34)
Browse files Browse the repository at this point in the history
* feat: outrospector v0 ok

* feat: outrospection v1 ok

* fix: outrospection arg type nesting

* refactor: rename outrospector to outrospect

* feat: add query&mutation type names to outrospection

* feat: formatter & converter

* refactor: clean lib.ts

* refactor: re-organize directories and update cli before merge

* feat: update readme for v1.1.0

* fix: lint before merge
  • Loading branch information
nohehf authored Sep 22, 2022
1 parent 4bf5200 commit 8267066
Show file tree
Hide file tree
Showing 10 changed files with 665 additions and 373 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"deno.enable": true,
"deno.unstable": true,
"editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ CLI from the file url._ To get started:

1. [Install deno](https://deno.land/#installation)
2. Run:
`deno run https://deno.land/x/graphman@v1.0.3/src/index.ts <graphql endpoint url>`
`deno run https://deno.land/x/graphman@v1.1.0/src/lib.ts <graphql endpoint url>`
3. Import the generated `[...].postman_collection.json` file in postman.

### Install GraphMan
Expand All @@ -50,7 +50,7 @@ If you want to access graphman easly you can "install" it on your machine:

1. [Install deno](https://deno.land/#installation)
2. Run:
`deno install -r -f --allow-net --allow-write -n graphman https://deno.land/x/graphman@v1.0.3/src/index.ts`
`deno install -r -f --allow-net --allow-write -n graphman https://deno.land/x/graphman@v1.1.0/src/lib.ts`
3. The command will output `export PATH="..."` copy paste it in your `~/.bashrc`
or `~/.zshrc` file to add graphman to your path. You can now run graphman
using the `graphman <params>` command! 🎉 To **update** GraphMan just
Expand Down Expand Up @@ -82,7 +82,7 @@ You can try graphman on public graphql APIs, and it is a great way to get
started with graphQL:

- Rick&Morty API:
`deno run https://deno.land/x/graphman@v1.0.3/src/index.ts https://rickandmortyapi.com/graphql`
`deno run https://deno.land/x/graphman@v1.1.0/src/lib.ts https://rickandmortyapi.com/graphql`

| <img width="300" src="https://raw.githubusercontent.com/Escape-Technologies/graphman/main/collection-example.png"> |
| :----------------------------------------------------------------------------------------------------------------: |
Expand Down
48 changes: 48 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { parse } from "https://deno.land/std@0.149.0/flags/mod.ts";
import { saveJsonFormatted } from "./lib.ts";
import { ensureDirSync } from "https://deno.land/std@0.151.0/fs/mod.ts";
import { createPostmanCollection } from "./index.ts";

// @TODO: improve the CLI

function help() {
console.log(`Error: not enough arguments.
Usage:
deno run index.ts <GRAPHQL_ENDPOINT_URL> {--out=OUTPUT_FILE, --authorization=AUTHORIZATION_HEADER}
Help:
deno run index.ts [--help | -h]
`);
}

const args = parse(Deno.args, { boolean: ["help", "h"] }) as {
_: [string];
help?: boolean;
h?: boolean;
out?: string;
auth?: string;
};

if (Deno.args.length < 1 || args.help || args.h) {
help();
Deno.exit(1);
}

const url = args._[0];
let path = args.out;
const authorization = args.auth;

const urlRegexp = /https?:\/\/*/;
if (!urlRegexp.test(url)) {
console.error(`${url} is not a valid url`);
Deno.exit(1);
}
console.log(`Creating the postman collection for ${url}`);

const collection = await createPostmanCollection(url, authorization);

path = path || "./out/" + collection.info.name + ".postman_collection.json";
ensureDirSync("./out/");
saveJsonFormatted(collection, path);
console.log(`Collection saved at ${path}`);

console.log(`Import it in postman and complete the queries ! 🚀`);
236 changes: 236 additions & 0 deletions src/converters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { parse, print } from "https://esm.sh/v90/graphql@16.5.0";
import { Argument, ObjectField, Outrospection, Query } from "./outrospector.ts";

export interface FormattedArgument {
formattedType: string;
formattedVariable: string;
defaultValue: string | "null" | "#";
}

export interface FormattedField {
formatedField: string;
tempField: string; // a hash that will be replaced by the formatedField, after the query is parsed, to keep comments
}

export interface FormattedQuery {
args: FormattedArgument[];
fields: FormattedField[];
fullQuery: string;
variables: string;
outrospectQuery: Query;
}

export interface QueryCollection {
queries: FormattedQuery[];
mutations: FormattedQuery[];
}

// @TODO: rework this
function getDefaultValue(type: string) {
switch (type) {
case "ID":
return `"0"`;
case "STRING" || "String":
return `""`;
case "INT" || "Int":
return `0`;
case "FLOAT":
return `0.0`;
case "BOOLEAN":
return `false`;
case "INPUT_OBJECT":
return `{}`;
default:
return `null`;
}
}

function nestedArgToString(arg: Argument, argStr?: string): string {
if (!argStr) argStr = arg.typeName;
if (arg.nesting.length === 0) {
return argStr;
} else {
const nesting = arg.nesting.pop();
if (nesting === "LIST") {
return `[${nestedArgToString(arg, argStr)}]`;
} else if (nesting === "NON_NULL") {
return `${nestedArgToString(arg, argStr)}!`;
}
}
return argStr;
}

function formatArgument(arg: Argument): FormattedArgument {
const formattedType: string = nestedArgToString(arg);

const defaultNonNullValue = formattedType
.replace(arg.typeName, getDefaultValue(arg.typeName))
.replaceAll("!", "");
const defaultValue = formattedType.includes("!")
? defaultNonNullValue
: "null";

return {
defaultValue,
formattedType,
formattedVariable: `\t"${arg.name}": ${defaultValue}`,
};
}

function formatField(field: ObjectField): FormattedField {
let description = "";
if (
field.description &&
field.description !== "undefined" &&
field.description !== ""
) {
description = ` # ${field.description?.replace("\n", " ")}`;
}

function scalarFormat(field: ObjectField) {
return `${field.name}${description}\n`;
}

function objectFormat(field: ObjectField) {
return `# ${field.name}${description}\n`;
}

function othersFormat(field: ObjectField) {
return `# ${field.name}${description} # Type: ${field.typeBaseKind}\n`;
}

let formattedFieldStr = "";
if (field.typeBaseKind === "SCALAR" || field.typeBaseKind === "ENUM") {
formattedFieldStr = scalarFormat(field);
} else if (field.typeBaseKind === "OBJECT") {
formattedFieldStr = objectFormat(field);
} else {
formattedFieldStr = othersFormat(field);
}

const formattedField: FormattedField = {
formatedField: formattedFieldStr,
tempField: `_${crypto.randomUUID().split("-")[0]}\n`,
};

return formattedField;
}

class FormattedFieldBuffer {
formattedFields = new Map<string, FormattedField>();
outrospection: Outrospection;

constructor(outrospection: Outrospection) {
this.outrospection = outrospection;
}

formatField(field: ObjectField): FormattedField {
if (this.formattedFields.get(field.name)) {
return this.formattedFields.get(field.name) as FormattedField;
} else {
const formattedField = formatField(field);
this.formattedFields.set(field.name, formattedField);
return formattedField;
}
}
}

function formatQuery(
query: Query,
is: "query" | "mutation",
outrospection: Outrospection,
): FormattedQuery {
let queryVarsDefinition = "";
let fieldVars = "";
let variables = "";

const formattedFieldBuffer = new FormattedFieldBuffer(outrospection);

const formattedQuery: FormattedQuery = {
args: [],
fields: [],
fullQuery: "",
variables: "",
outrospectQuery: query,
};

Array.from(query.args).forEach(([_, arg], index) => {
const formatedArg = formatArgument(arg);
formattedQuery.args.push(formatedArg);
queryVarsDefinition += `${index === 0 ? "" : ","}${
query.args.size > 3 ? "\n" : " "
}$${arg.name}: ${formatedArg.formattedType}`;

fieldVars += `${index === 0 ? "" : ", "}${
query.args.size > 3 ? "\n" : " "
}${arg.name}: $${arg.name}`;

variables += `${index === 0 ? "" : ",\n"}${formatedArg.formattedVariable}`;
});

formattedQuery.variables = `{\n${variables}\n}`;

if (query.args.size > 3) {
queryVarsDefinition += "\n";
fieldVars += "\n";
}

let formatedFields = "__typename\n";

const returnType = outrospection.types.get(query.typeName);
if (!returnType) {
throw new Error(`Type ${query.typeName} not found in outrospection`);
}
if (returnType && returnType.kind === "OBJECT") {
returnType.fields?.forEach((field) => {
const formatedField = formattedFieldBuffer.formatField(field);
formattedQuery.fields.push(formatedField);
formatedFields += formatedField.tempField;
});
}

const hasArgs = query.args.size > 0;
const hasFields = returnType.kind === "OBJECT" &&
returnType.fields && returnType.fields?.length > 0;

const parsed = parse(
`${is} ${query.name}${
hasArgs ? `(${queryVarsDefinition})` : ""
}{\n${query.name}${hasArgs ? `(${fieldVars})` : ""}${
hasFields ? `{\n${formatedFields}}` : ""
}\n}`,
);

formattedQuery.fullQuery = print(parsed);

if (returnType.kind === "OBJECT") {
returnType.fields?.forEach((field) => {
const formatedField = formattedFieldBuffer.formatField(field);
formattedQuery.fullQuery = formattedQuery.fullQuery.replace(
formatedField.tempField,
formatedField.formatedField,
);
});
}

return formattedQuery;
}

export function outrospectionToQueries(
outrospection: Outrospection,
): QueryCollection {
const collection: QueryCollection = {
queries: [],
mutations: [],
};

outrospection.queries.forEach((query) => {
collection.queries.push(formatQuery(query, "query", outrospection));
});

outrospection.mutations.forEach((query) => {
collection.queries.push(formatQuery(query, "mutation", outrospection));
});

return collection;
}
Loading

0 comments on commit 8267066

Please sign in to comment.