megaera
Version:
GraphQL to TypeScript Generator
151 lines (148 loc) • 4.76 kB
JavaScript
import { isEnumType, isListType, isNonNullType, isNullableType, isScalarType, } from 'graphql/type/definition.js';
import { isObjectType } from 'graphql/type/index.js';
export function generate(content) {
const code = [];
for (const f of content.fragments.values()) {
code.push(`const ${f.name} = \`#graphql
${f.source}\`
export type ${f.name} = ${generateSelector(f, 0, true)}
`);
}
for (const q of content.operations) {
if (!q.source) {
throw new Error('Empty query source for operation ' + q.name);
}
let querySource = q.source;
for (const fName of usedFragments(q, content)) {
querySource = '${' + fName + '}\n' + querySource;
}
code.push(`export const ${q.name} = \`#graphql
${querySource}\` as string & ${q.name}
export type ${q.name} = (${generateVariables(q.variables)}) => ${generateSelector(q, 0, true)}
`);
}
return code.join('\n');
}
function usedFragments(q, content) {
const fragments = [];
for (const field of q.fields) {
if (field.isFragment) {
fragments.push(field.name);
const fragment = content.fragments.get(field.name);
if (!fragment) {
throw new Error(`Fragment ${field.name} is not defined.`);
}
fragments.push(...usedFragments(fragment, content));
}
fragments.push(...usedFragments(field, content));
}
for (const inlineFragment of q.inlineFragments) {
fragments.push(...usedFragments(inlineFragment, content));
}
return fragments;
}
function generateVariables(variables) {
if (!variables || variables.length === 0) {
return '';
}
return ('vars: { ' +
variables
.map((v) => {
return v.name + (v.required ? '' : '?') + ': ' + generateType(v.type);
})
.join(', ') +
' }');
}
function generateSelector(s, depth = 0, nonNull = false) {
if (s.fields.length === 0 && s.inlineFragments.length === 0) {
return generateType(s.type);
}
const code = generateFields(s, depth) +
generateFragments(s) +
generateInlineFragments(s, depth - 1);
if (isNonNullType(s.type)) {
return wrapInArray(s.type.ofType, code);
}
return (wrapInArray(s.type, code) +
(isNullableType(s.type) && !nonNull ? ' | null' : ''));
}
function wrapInArray(t, code) {
return isListType(t) ? `Array<${code}>` : code;
}
function generateFragments(s) {
let code = '';
for (const fragment of s.fields) {
if (!fragment.isFragment)
continue;
code += ' & ' + fragment.name;
}
return code;
}
function generateFields(s, depth) {
const nonFragmentFields = s.fields.filter((f) => !f.isFragment);
if (nonFragmentFields.length === 0) {
return '{}';
}
const code = [];
code.push('{');
for (const field of nonFragmentFields) {
code.push(' '.repeat(depth + 1) +
field.name +
': ' +
generateSelector(field, depth + 1));
}
code.push(' '.repeat(depth) + '}');
return code.join('\n');
}
function generateInlineFragments(s, depth) {
let code = '';
let nullable = false;
for (const fragment of s.inlineFragments) {
code += ' & ' + generateSelector(fragment, depth + 1, true);
nullable ||= isNullableType(fragment.type);
}
return code + (nullable ? ' | null' : '');
}
export function generateType(t, orNull = ' | null') {
if (t === undefined) {
return 'unknown';
}
if (isNonNullType(t)) {
return generateType(t.ofType, '');
}
if (isListType(t)) {
const subType = generateType(t.ofType);
if (subType.includes(' ')) {
return '(' + subType + ')[]' + orNull;
}
return subType + '[]' + orNull;
}
if (isEnumType(t)) {
return t
.getValues()
.map((v) => `'${v.value}'`)
.join(' | ');
}
if (isObjectType(t)) {
const code = [];
for (const field of Object.values(t.getFields())) {
code.push(field.name + ': ' + generateType(field.type));
}
return '{' + code.join(', ') + '}';
}
if (isScalarType(t)) {
switch (t.name) {
case 'String':
return 'string' + orNull;
case 'Int':
return 'number' + orNull;
case 'Float':
return 'number' + orNull;
case 'Boolean':
return 'boolean' + orNull;
default:
return 'string' + orNull;
}
}
throw new Error(`Cannot generate TypeScript type from GraphQL type "${t}".`);
}