charlotte-graphql
Version:
Generates GraphQL type definitions and resolvers off of a concise spec.
147 lines (116 loc) • 3.85 kB
JavaScript
;
const _ = require('lodash');
const pluralize = require('pluralize');
const scalarTypeNames = ['String', 'ID', 'Int', 'Float', 'Boolean'];
const defaultFields = {
uuid: 'ID!',
_created: 'String!',
_updated: 'String!'
};
const createType = typeName => {
let plural = pluralize(typeName);
const isScalar = isScalarType(typeName);
if (typeName === plural) {
plural = plural + '_s';
}
return {
name: typeName,
nameCamel: _.camelCase(typeName),
plural,
pluralCamel: _.camelCase(plural),
fields: {},
isScalar: isScalar
};
};
const expandTypes = source => {
const expanded = scalarTypeNames.reduce((idx, typeName) => {
return Object.assign(idx, {
[typeName]: createType(typeName)
});
}, {});
const queue = [];
const dequeue = (cursor = 0) => {
const f = queue[cursor];
if (f) {
f();
dequeue(cursor + 1);
}
};
// Expand source types into a tree of referencing types and fields
_.each(source, (_sourceFields, typeName) => {
const type = expanded[typeName] = createType(typeName);
const sourceFields = Object.assign({}, defaultFields, _sourceFields);
_.each(sourceFields, (typeDescriptor, fieldName) => {
const isNullable = typeDescriptor.slice(-1) !== '!';
const nullableTypeName = unrequire(typeDescriptor);
const field = type.fields[fieldName] = {
name: fieldName,
parentType: type,
typeDescriptor,
type: null,
reciprocal: null,
reciprocalOf: null,
isNullable,
appendRequirement: str => str + (isNullable? '': '!')
};
if (expanded[nullableTypeName]) {
field.type = expanded[nullableTypeName];
} else {
queue.push(() => {
field.type = expanded[nullableTypeName];
if (!field.type) {
throw new Error(`${typeName}.${fieldName} refers to type "${nullableTypeName}", which is not defined`)
}
});
}
if (isScalarType(typeDescriptor)) {
return;
}
const addReciprocals = () => {
const siblingFields = Object
.values(field.parentType.fields)
.filter(_field => _field.name !== field.name);
const relatedFieldsStillNotResolved = siblingFields.some(_field => !_field.type);
if (relatedFieldsStillNotResolved) {
queue.push(addReciprocals);
return;
}
const fieldNameWithIntStripped = field.name.replace(/[0-9]+$/, '');
const siblingsWithType = siblingFields.filter(_field => {
return _field.type.name === field.type.name;
});
const combinableSiblings = siblingsWithType.filter(_field => {
return _field.name.replace(/[0-9]+$/, '') === fieldNameWithIntStripped;
});
const _name = combinableSiblings.length === siblingsWithType.length
? type.pluralCamel
: siblingsWithType.length === 0
? type.pluralCamel
: type.pluralCamel + _.upperFirst(fieldNameWithIntStripped);
const _typeDescriptor = `[${field.parentType.name}!]!`;
field.reciprocal = field.type.fields[_name] = {
name: _name,
parentType: field.type,
typeDescriptor: _typeDescriptor,
type: field.parentType,
reciprocalOf: [field].concat(combinableSiblings),
reciprocal: null,
isNullable: false,
appendRequirement: str => str.replace('!', '') + '!'
}
};
queue.push(addReciprocals);
});
});
dequeue();
return _.omit(expanded, scalarTypeNames);
};
const isScalarType = type => {
return new RegExp(`^(${scalarTypeNames.join('|')})!?$`).test(type);
};
const unrequire = str => str.replace('!', '');
module.exports = {
expandTypes,
isScalarType,
unrequire
};