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

feat: generate variables when we copy a persisted operation #552

Merged
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
108 changes: 105 additions & 3 deletions studio/src/__tests__/schema-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import { parse, OperationDefinitionNode, Kind } from "graphql";
import {
getDeprecatedTypes,
getTypeCounts,
formatAndParseSchema,
extractVariablesFromGraphQL,
parseSchema,
} from "../lib/schema-helpers";
import { expect, test } from "vitest";

const schema = `
type Query {
employees: [Employee!]!
teammates(team: Department! @deprecated): [Employee!]!
findID(criteria: Criteria!): Int!
}

input Criteria {
age: Int!
nested: Nested
hasPets: Boolean
}

input Nested {
department: Department!
}

enum Department {
Expand Down Expand Up @@ -39,14 +53,102 @@ test("return the correct types with deprecated fields or args", async () => {
expect(deprecated[1].fields?.[0]?.name).toEqual("fullName");
});

test("returns correct type counts", async () => {
const ast = await formatAndParseSchema(schema);
test("returns correct type counts", () => {
const ast = parseSchema(schema);

expect(ast).not.toBeNull();

const counts = getTypeCounts(ast!);

expect(counts["query"]).toEqual(2);
expect(counts["query"]).toEqual(3);
expect(counts["objects"]).toEqual(1);
expect(counts["enums"]).toEqual(1);
});

test("returns empty if no variables are present", () => {
const ast = parseSchema(schema);
expect(ast).not.toBeNull();

const query = `
query {
employees {
id
}
}
`;

const variables = extractVariablesFromGraphQL(query, ast);
expect(variables).toMatchObject({});
});

test("returns multiple variables", () => {
const ast = parseSchema(schema);
expect(ast).not.toBeNull();

const query = `
query ($a: Int, $criteria: Criteria!) {
employees {
id
}
}
`;

const variables = extractVariablesFromGraphQL(query, ast);
expect(variables).toMatchObject({
a: 0,
criteria: {
age: 0,
nested: {
department: "ENGINEERING",
},
},
});
});

test("returns multiple variables with defaults", () => {
const ast = parseSchema(schema);
expect(ast).not.toBeNull();

const query = `
query ($a: Int = 10, $criteria: Criteria = { age: 12, hasPets: true, nested: { department: "ENGINEERING" }}, $b: [Int] = [1,2,3]) {
employees {
id
}
}
`;

const variables = extractVariablesFromGraphQL(query, ast);

expect(variables).toMatchObject({
a: 10,
criteria: {
age: 12,
nested: {
department: "ENGINEERING",
},
hasPets: true,
},
b: [1, 2, 3],
});
});

test("returns nested variables", () => {
const ast = parseSchema(schema);
expect(ast).not.toBeNull();

const query = `
query ($criteria: Criteria) {
findID(criteria: $criteria)
}
`;

const variables = extractVariablesFromGraphQL(query, ast);
expect(variables).toMatchObject({
criteria: {
age: 0,
nested: {
department: "ENGINEERING",
},
},
});
});
136 changes: 136 additions & 0 deletions studio/src/lib/schema-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import {
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLList,
GraphQLNamedType,
GraphQLObjectType,
GraphQLScalarType,
GraphQLSchema,
GraphQLUnionType,
Kind,
Location,
buildASTSchema,
isObjectType,
isScalarType,
parse,
} from "graphql";
import babelPlugin from "prettier/plugins/babel";
Expand Down Expand Up @@ -156,6 +160,138 @@ export const mapGraphQLType = (
throw new Error("Unsupported GraphQL type");
};

export const extractVariablesFromGraphQL = (
body: string,
ast: GraphQLSchema | null,
) => {
const allTypes = ast
? Object.values(ast.getTypeMap())
.filter((type) => !type.name.startsWith("__"))
.sort()
: [];

let variables: Record<string, any> = {};

const parsedOp = parse(body);

if (parsedOp.definitions[0].kind === Kind.OPERATION_DEFINITION) {
parsedOp.definitions[0].variableDefinitions?.forEach((vd) => {
const variableName = vd.variable.name.value;
let type = "";

if (vd.type.kind === Kind.NON_NULL_TYPE) {
if (vd.type.type.kind === Kind.NAMED_TYPE) {
type = vd.type.type.name.value;
}
} else if (vd.type.kind === Kind.NAMED_TYPE) {
type = vd.type.name.value;
}

let defaultValueParsed;

if (vd.defaultValue) {
defaultValueParsed = parseDefaultValue(vd.defaultValue, allTypes);
} else {
defaultValueParsed = getDefaultValue(type, allTypes);
}
variables[variableName] = defaultValueParsed;
});
}

return variables;
};

function parseDefaultValue(defaultValue: any, allTypes: any[]): any {
switch (defaultValue.kind) {
case Kind.INT:
return parseInt(defaultValue.value);
case Kind.FLOAT:
return parseFloat(defaultValue.value);
case Kind.STRING:
case Kind.BOOLEAN:
case Kind.ENUM:
return defaultValue.value;
case Kind.LIST:
return defaultValue.values.map((val: any) =>
parseDefaultValue(val, allTypes),
);
case Kind.OBJECT:
const objValue: Record<string, any> = {};
defaultValue.fields.forEach((field: any) => {
const fieldName = field.name.value;
const fieldType = allTypes.find((type) => type.name === fieldName);
const fieldValue = parseDefaultValue(field.value, allTypes);
objValue[fieldName] = fieldType
? castToType(fieldType, fieldValue)
: fieldValue;
});
return objValue;
case Kind.NULL:
return null;
default:
return undefined;
}
}

// Helper function to cast field value to its respective type
function castToType(fieldType: any, fieldValue: any): any {
if (isScalarType(fieldType)) {
if (fieldType.name === "Int") {
return parseInt(fieldValue);
} else if (fieldType.name === "Float") {
return parseFloat(fieldValue);
} else if (fieldType.name === "Boolean") {
return fieldValue === "true";
} else {
return fieldValue;
}
} else {
return fieldValue;
}
}

function getDefaultValue(
typeName: string,
schemaTypes: GraphQLNamedType[],
): any {
const isNonNull = typeName.endsWith("!");
if (isNonNull) {
typeName = typeName.slice(0, -1);
}

const foundType = schemaTypes.find((type) => type.name === typeName);
if (!foundType) return null;

if (foundType instanceof GraphQLScalarType) {
switch (foundType.name) {
case "Int":
case "Float":
return 0;
case "Boolean":
return false;
case "ID":
case "String":
return "";
default:
return null;
}
} else if (foundType instanceof GraphQLInputObjectType) {
const fields = foundType.getFields();
const fieldDefaults: Record<string, any> = {};
Object.entries(fields).forEach(([fieldName, field]) => {
fieldDefaults[fieldName] = getDefaultValue(
field.type.toString(),
schemaTypes,
);
});
return fieldDefaults;
} else if (foundType instanceof GraphQLEnumType) {
return foundType.getValues()[0]?.value || null;
} else {
return null;
}
}

export const getTypesByCategory = (
astSchema: GraphQLSchema,
category: GraphQLTypeCategory,
Expand Down
Loading
Loading