import { GraphQLArgument, GraphQLInputObjectType, GraphQLList, GraphQLNamedType, GraphQLObjectType, GraphQLOutputType, GraphQLResolveInfo, GraphQLSchema, GraphQLType, IntrospectionObjectType, IntrospectionType, defaultFieldResolver, getNamedType, isEnumType, isInterfaceType, isObjectType, isScalarType, isUnionType } from 'graphql'; import { difference, each, eq, find, get, isArray, isEmpty, isObject, keys, map, set, union } from 'lodash'; import pluralize from 'pluralize'; import { Connection, DataResolver } from './GraphQLGenieInterfaces'; import { FindByUniqueError, getReturnType, typeIsList } from './GraphQLUtils'; export class Relation { public type0: string; public field0: string; public field0isList: boolean; public type1: string; public field1: string; public field1isList: boolean; constructor($type: string, $field: string, $field0isList: boolean) { this.type0 = $type; this.field0 = $field; this.field0isList = $field0isList; } setRelative(relation: Relation) { this.type1 = relation.type0; this.field1 = relation.field0; this.field1isList = relation.field0isList; } isValidRelative(relation: Relation) { if (!this.type1) { return true; } else { return this.isSameRelation(relation) || this.isCurrentRelation(relation); } } isSameRelation(relation: Relation): boolean { return this.type0 === relation.type0 && this.field0 === relation.field0 && this.field0isList === relation.field0isList; } isCurrentRelation(relation: Relation): boolean { return this.type1 === relation.type0 && this.field1 === relation.field0 && this.field1isList === relation.field0isList; } getInverse(type: string, field: string, inverseType?: string): string { const inverse = this.getInverseTuple(type, field, inverseType); return inverse ? inverse[1] : null; } getInverseTuple(type: string, field: string, inverseType?: string): [string, string] { let inverse = null; if (this.type0 === type && this.field0 === field && (!inverseType || this.type1 === inverseType)) { inverse = [this.type1, this.field1]; } else if (this.type1 === type && this.field1 === field && (!inverseType || this.type0 === inverseType)) { inverse = [this.type0, this.field0]; } return inverse; } } export class Relations { public relations: Map; constructor() { this.relations = new Map(); } public getRelation(name: string): Relation { let relations = null; if (this.relations.has(name)) { relations = this.relations.get(name); } return relations; } public getInverseWithoutName(type: string, field: string, inverseType: string): string { let inverse: string = null; const iter = this.relations.values(); let relation = iter.next().value; while (!inverse && relation) { inverse = relation.getInverse(type, field, inverseType); relation = iter.next().value; } return inverse; } public getInverse(name: string, type: string, field: string): string { let inverse = null; if (this.relations.has(name)) { const relation = this.relations.get(name); inverse = relation.getInverse(type, field); } return inverse; } public setRelation(name: string, type: string, field: string, fieldIsList: boolean) { const newRelation = new Relation(type, field, fieldIsList); if (!this.relations.has(name)) { this.relations.set(name, newRelation); } else { const relation = this.relations.get(name); if (relation.isValidRelative(newRelation)) { if (!relation.isSameRelation(newRelation)) { relation.setRelative(newRelation); } } else { this.throwError(name, type, field, relation.field0); } } } public setSelfRelation(name: string, type: string, field: string, fieldIsList: boolean) { const newRelation = new Relation(type, field, fieldIsList); newRelation.setRelative(newRelation); this.relations.set(name, newRelation); } private throwError(name: string, type: string, primaryField: string, relatedField: string) { throw new Error(`Bad schema, relation could apply to multiple fields relation name: ${name} fortune name: ${type} curr field: ${primaryField} other field: ${relatedField}`); } } const computeNumFieldsOfType = (type: IntrospectionObjectType, checkFieldTypeName: string): number => { let resultNum = 0; each(type.fields, field => { if (checkFieldTypeName === getReturnType(field.type)) { resultNum++; } }); return resultNum; }; const getNumFieldsOfType = (cache: Map>, type: IntrospectionObjectType, checkFieldTypeName: string): number => { let numFields = 0; const typeName = getReturnType(type); if (cache.has(typeName) && cache.get(typeName).has(checkFieldTypeName)) { numFields = cache.get(typeName).get(checkFieldTypeName); } else { numFields = computeNumFieldsOfType(type, checkFieldTypeName); if (!cache.has(typeName)) { cache.set(typeName, new Map()); } cache.get(typeName).set(checkFieldTypeName, numFields); } return numFields; }; export const computeRelations = (schemaInfo: IntrospectionType[], typeNameResolver: (name: string) => string = (name: string) => name): Relations => { const numFieldsOfTypeCache = new Map>(); const relations = new Relations(); each(keys(schemaInfo), (typeName) => { const type = schemaInfo[typeName]; each(type.fields, field => { const relation = get(field, 'metadata.relation'); const fieldTypeName = getReturnType(field.type); const reslovedTypeName = typeNameResolver(fieldTypeName); if (relation) { relations.setRelation(relation.name, reslovedTypeName, field.name, typeIsList(field.type)); } else if (typeName === fieldTypeName) { relations.setSelfRelation(`${field.name}On${typeName}`, reslovedTypeName, field.name, typeIsList(field.type)); } else { const fieldTypeInfo = schemaInfo[fieldTypeName]; if (type && fieldTypeInfo) { const numFields = getNumFieldsOfType(numFieldsOfTypeCache, type, fieldTypeName); const reverseNumFields = getNumFieldsOfType(numFieldsOfTypeCache, fieldTypeInfo, typeName); if (numFields === 1 && reverseNumFields === 1) { const possibleTypes = [typeName, fieldTypeName]; possibleTypes.sort(); relations.setRelation(possibleTypes.join('_'), reslovedTypeName, field.name, typeIsList(field.type)); } } } }); }); return relations; }; export enum Mutation { Create, Update, Delete, Upsert } export const clean = (obj): any => { const returnObj = {}; for (const propName in obj) { if (obj[propName] !== null && obj[propName] !== undefined) { // tslint:disable-next-line:prefer-conditional-expression if (isObject(obj[propName]) && !isEmpty(obj[propName])) { returnObj[propName] = obj[propName]; } else { returnObj[propName] = obj[propName]; } } } return returnObj; }; const setupArgs = (results: any[], args: any[]) => { // setup the arguments to use the new types results.forEach((types: any[]) => { types = types ? types : []; types.forEach(type => { if (type && type.key && type.id && type.index > -1) { const key = type.key; const id = type.id; const arg = args[type.index]; if (isArray(arg[key])) { if (isArray(id)) { arg[key] = union(id, arg[key]); } else if (!arg[key].includes(id)) { arg[key].push(id); } } else { arg[key] = id; } } }); }); return args; }; const resolveArgs = async (args: Array, returnType: GraphQLOutputType, mutation: Mutation, dataResolver: DataResolver, currRecord: any, _args: { [key: string]: any }, _context: any, _info: GraphQLResolveInfo): Promise> => { const promises: Array> = []; args.forEach((currArg, index) => { for (const argName in currArg) { let argReturnType: GraphQLOutputType; let argReturnRootType: GraphQLOutputType; if ((isObjectType(returnType) || isInterfaceType(returnType)) && returnType.getFields()[argName]) { argReturnType = returnType.getFields()[argName].type; argReturnRootType = getNamedType(argReturnType); if (argReturnRootType['name'].endsWith('Connection')) { argReturnRootType = _info.schema.getType(argReturnRootType['name'].replace(/Connection$/g, '')); argReturnType = new GraphQLList(argReturnRootType); } } if (argReturnRootType && !isScalarType(argReturnRootType) && !isEnumType(argReturnRootType)) { const arg = currArg[argName]; if (isObject(arg) && argReturnType) { currArg[argName] = typeIsList(argReturnType) ? [] : undefined; if (isInterfaceType(argReturnRootType) || isUnionType(argReturnRootType)) { for (const argKey in arg) { const argTypeName = pluralize.singular(argKey).toLowerCase(); argReturnRootType = find(_info.schema.getTypeMap(), type => { return type.name.toLowerCase() === argTypeName; }); promises.push(mutateResolver(mutation, dataResolver)(currRecord, arg[argKey], _context, _info, index, argName, argReturnRootType)); } } else { promises.push(mutateResolver(mutation, dataResolver)(currRecord, arg, _context, _info, index, argName, argReturnRootType)); } } } } }); const results = await Promise.all(promises); args = setupArgs(results, args); return args; }; const mutateResolver = (mutation: Mutation, dataResolver: DataResolver) => { return async (currRecord: any, _args: { [key: string]: any }, _context: any, _info: GraphQLResolveInfo, index?: number, key?: string, returnType?: GraphQLOutputType) => { await dataResolver.beginTransaction(); // iterate over all the non-id arguments and recursively create new types const recursed = returnType ? true : false; if (!returnType) { returnType = (_info.returnType).getFields().data.type; returnType = getNamedType(returnType); } const returnTypeName = getReturnType(returnType); const clientMutationId = _args.input && _args.input.clientMutationId ? _args.input.clientMutationId : ''; let createArgs = _args.create ? _args.create : mutation === Mutation.Create && get(_args, 'input.data') ? get(_args, 'input.data') : []; createArgs = createArgs && !isArray(createArgs) ? [createArgs] : createArgs; let updateArgs = _args.update ? _args.update : mutation === Mutation.Update && get(_args, 'input.data') ? get(_args, 'input.data') : []; updateArgs = updateArgs && !isArray(updateArgs) ? [updateArgs] : updateArgs; let upsertArgs = _args.upsert ? _args.upsert : mutation === Mutation.Upsert && get(_args, 'input') ? get(_args, 'input') : []; upsertArgs = upsertArgs && !isArray(upsertArgs) ? [upsertArgs] : upsertArgs; let deleteArgs = _args.delete ? _args.delete : mutation === Mutation.Delete && _args.input.where ? _args.input.where : []; deleteArgs = deleteArgs && !isArray(deleteArgs) ? [deleteArgs] : deleteArgs; let connectArgs = _args.connect ? _args.connect : []; connectArgs = connectArgs && !isArray(connectArgs) ? [connectArgs] : connectArgs; let disconnectArgs = _args.disconnect ? _args.disconnect : []; disconnectArgs = disconnectArgs && !isArray(disconnectArgs) ? [disconnectArgs] : disconnectArgs; const whereArgs = _args.where ? _args.where : _args.input && _args.input.where ? _args.input.where : null; const conditionsArgs = _args.conditions ? _args.conditions : _args.input && _args.input.conditions ? _args.input.conditions : null; // lets make sure we are able to add this (prevent duplicates on unique fields, etc) const canAddResults = await Promise.all([dataResolver.canAdd(returnTypeName, createArgs, {context: _context, info: _info}), dataResolver.canAdd(returnTypeName, updateArgs, {context: _context, info: _info})]); const cannotAdd = canAddResults.includes(false); if (cannotAdd) { throw new Error('Can not create record with duplicate on unique field on type ' + returnTypeName + ' ' + JSON.stringify(createArgs) + ' ' + JSON.stringify(updateArgs)); } const dataResolverPromises: Array> = []; if (!isEmpty(updateArgs)) { if (whereArgs) { // we have a where so use that to get the record to update // pass true to where args if currRecord is already the one we want if (whereArgs !== true) { const returnTypeName = getReturnType(returnType); currRecord = await dataResolver.getValueByUnique(returnTypeName, whereArgs, {context: _context, info: _info}); if (!currRecord || isEmpty(currRecord)) { throw new FindByUniqueError(`${returnTypeName} does not exist with where args ${JSON.stringify(whereArgs)}`, 'update', {arg: whereArgs, typename: returnTypeName}); } } } else if (updateArgs[0].data && updateArgs[0].where) { // this is a nested update an a list type so we need to individually do updates updateArgs.forEach((currArg) => { dataResolverPromises.push( new Promise((resolve, reject) => { mutateResolver(mutation, dataResolver)(currRecord, { update: currArg.data, where: currArg.where }, _context, _info, index, key, returnType).then((result) => { if (recursed) { resolve(); } else { resolve(result[0]); } }).catch(reason => { reject(reason); }); }) ); }); updateArgs = []; } else if (key && currRecord) { // this is a nested input on a single field so we already know the where const recordToUpdate = await dataResolver.getValueByUnique(returnTypeName, { id: currRecord[key] }, {context: _context, info: _info}); if (recordToUpdate) { currRecord = recordToUpdate; } else { // trying to update an empty field updateArgs = []; } } } if (!isEmpty(upsertArgs)) { await Promise.all(upsertArgs.map(async (currArg) => { const whereArg = currArg.where; let upsertRecord = currRecord; if (whereArg) { // this is a root upsert or nested upsert with a where field upsertRecord = await dataResolver.getValueByUnique(returnTypeName, whereArg, {context: _context, info: _info}); } else if (upsertRecord && key) { // this is a nested upsert on a single field so we already have the where upsertRecord = upsertRecord[key] ? await dataResolver.getValueByUnique(returnTypeName, { id: upsertRecord[key] }, {context: _context, info: _info}) : null; } let newArgs: object = { create: currArg.create }; if (upsertRecord && !isEmpty(upsertRecord)) { // pass true to where args if currRecord will already be the one we want newArgs = { where: true, update: currArg.update, conditions: conditionsArgs }; } dataResolverPromises.push( new Promise((resolve, reject) => { mutateResolver(mutation, dataResolver)(upsertRecord, newArgs, _context, _info, index, key, returnType).then((result) => { if (result[0]) { resolve(result[0]); } else { resolve(); } }).catch(reason => { reject(reason); }); }) ); })); } [createArgs, updateArgs] = await Promise.all([ resolveArgs(createArgs, returnType, Mutation.Create, dataResolver, currRecord, _args, _context, _info), resolveArgs(updateArgs, returnType, Mutation.Update, dataResolver, currRecord, _args, _context, _info) ]); // could be creating more than 1 type createArgs.forEach((createArg) => { createArg = createArg.hasOwnProperty ? createArg : Object.assign({}, createArg); createArg = clean(createArg); if (createArg && !isEmpty(createArg)) { dataResolverPromises.push(new Promise((resolve, reject) => { dataResolver.create(returnTypeName, createArg, {context: _context, info: _info}).then(data => { const id = isArray(data) ? map(data, 'id') : data.id; resolve({ index, key, id, data }); }).catch(reason => { reject(reason); }); })); } }); // now updates updateArgs.forEach((updateArg) => { // make sure it is prototype correctly to prevent error updateArg = updateArg.hasOwnProperty ? updateArg : Object.assign({}, updateArg); // only do updates on new values for (const updateArgKey in updateArg) { const currArg = updateArg[updateArgKey]; const currRecordArg = currRecord[updateArgKey]; if (eq(currRecordArg, currArg)) { delete currRecord[updateArgKey]; } else if (isArray(currArg) && isArray(currRecordArg)) { // for relations we can't have duplicates, only relations will be arrays updateArg[updateArgKey] = difference(currArg, currRecordArg); } } const cleanArg = clean(updateArg); if (cleanArg && !isEmpty(cleanArg)) { dataResolverPromises.push(new Promise((resolve, reject) => { cleanArg.id = currRecord.id; const meta = {context: _context, info: _info}; meetsConditions(conditionsArgs, returnTypeName, returnType, currRecord, dataResolver, _context, _info).then(meetsConditionsResult => { if (!meetsConditionsResult) { resolve({ index, key, id: [], unalteredData: currRecord}); } else { dataResolver.update(returnTypeName, cleanArg, meta).then(data => { const id = isArray(data) ? map(data, 'id') : data.id; resolve({ index, key, id, data }); }).catch(reason => { reject(reason); }); } }).catch(reason => { reject(reason); }); })); } else if (currRecord) { currRecord = Object.assign(currRecord, updateArg); } }); // now add the connect types connectArgs.forEach(connectArg => { dataResolverPromises.push(new Promise((resolve, reject) => { dataResolver.getValueByUnique(returnTypeName, connectArg, {context: _context, info: _info}).then(data => { if (data && data['id']) { resolve({ index, key, id: data['id'], data }); } else { reject(new FindByUniqueError(`connect: ${returnTypeName} does not exist with where args ${JSON.stringify(connectArg)}`, 'disconnect', {arg: connectArg, typename: returnTypeName})); } }).catch(reason => { reject(reason); }); })); }); // disconnect const disconnectPromises: Array> = []; disconnectArgs.forEach(disconnectArg => { if (disconnectArg === true) { dataResolverPromises.push(new Promise((resolve, reject) => { dataResolver.update(currRecord.__typename, { id: currRecord.id, [key]: null }, {context: _context, info: _info}).then(data => { resolve({ index, key, id: null, data }); }).catch(reason => { reject(reason); }); })); } else { disconnectPromises.push(new Promise((resolve, reject) => { dataResolver.getValueByUnique(returnTypeName, disconnectArg, {context: _context, info: _info}).then(data => { if (data && data['id']) { resolve(data['id']); } else { reject(new FindByUniqueError(`disconnect: ${returnTypeName} does not exist with where args ${JSON.stringify(disconnectArg)}`, 'disconnect', {arg: disconnectArg, typename: returnTypeName})); } }).catch(reason => { reject(reason); }); })); } }); const disconnectIds = await Promise.all(disconnectPromises); if (!isEmpty(disconnectIds)) { dataResolverPromises.push(new Promise((resolve, reject) => { dataResolver.update(currRecord.__typename, { id: currRecord.id, [key]: disconnectIds }, {context: _context, info: _info}, { pull: true }).then(data => { resolve({ index, key, id: data[key], data }); }).catch(reason => { reject(reason); }); })); } // delete const deletePromises: Array> = []; deleteArgs.forEach(deleteArg => { if (deleteArg === true) { // nested singular delete dataResolverPromises.push(new Promise((resolve, reject) => { dataResolver.delete(dataResolver.getLink(currRecord.__typename, key), [currRecord[key]], {context: _context, info: _info}).then(data => { resolve({ index, key, id: null, data }); }).catch(reason => { reject(reason); }); })); } else if (whereArgs && !currRecord) { // delete resolver dataResolverPromises.push(new Promise((resolve, reject) => { dataResolver.getValueByUnique(returnTypeName, whereArgs, {context: _context, info: _info}).then(whereData => { currRecord = whereData; if (!currRecord || isEmpty(currRecord)) { throw new FindByUniqueError(`${returnTypeName} does not exist with where args ${JSON.stringify(whereArgs)}`, 'delete', {arg: whereArgs, typename: returnTypeName}); } meetsConditions(conditionsArgs, returnTypeName, returnType, currRecord, dataResolver, _context, _info).then(meetsConditionsResult => { if (!meetsConditionsResult) { resolve({ index, key, id: [], unalteredData: currRecord}); } else { dataResolver.delete(currRecord.__typename, [currRecord.id], {context: _context, info: _info}).then(() => { resolve({ index, key, id: null, data: currRecord }); }).catch(reason => { reject(reason); }); } }).catch(reason => { reject(reason); }); }).catch(reason => { reject(reason); }); })); } else { // nested delete on list deletePromises.push(new Promise((resolve, reject) => { const deleteTypeName = dataResolver.getLink(currRecord.__typename, key); dataResolver.getValueByUnique(deleteTypeName, deleteArg, {context: _context, info: _info}).then(data => { if (data && data['id']) { resolve(data['id']); } else { reject(new FindByUniqueError(`${deleteTypeName} does not exist with where args ${JSON.stringify(deleteArg)}`, 'delete', {arg: deleteArg, typename: deleteTypeName})); } }).catch(reason => { reject(reason); }); })); } }); const deleteIds = await Promise.all(deletePromises); if (!isEmpty(deleteIds)) { dataResolverPromises.push(new Promise((resolve, reject) => { dataResolver.delete(dataResolver.getLink(currRecord.__typename, key), deleteIds, {context: _context, info: _info}).then(data => { resolve({ index, key, id: data[key], data }); }).catch(reason => { reject(reason); }); })); } const dataResult = await Promise.all(dataResolverPromises); // if key this is recursed else it's the final value if (recursed) { return dataResult; } else { await dataResolver.endTransaction(); let data = get(dataResult, '[0].data'); const unalteredData = get(dataResult, '[0].unalteredData', null); if (!unalteredData && !data && mutation === Mutation.Delete) { data = currRecord; } else if (!unalteredData && !data) { // if everything was already done on the object (updates, deletions and disconnects) it should be the currRecord but with changes data = currRecord; } return { data, clientMutationId, unalteredData }; } }; }; export const createResolver = (dataResolver: DataResolver) => { return mutateResolver(Mutation.Create, dataResolver); }; export const updateResolver = (dataResolver: DataResolver) => { return mutateResolver(Mutation.Update, dataResolver); }; export const upsertResolver = (dataResolver: DataResolver) => { return mutateResolver(Mutation.Upsert, dataResolver); }; export const deleteResolver = (dataResolver: DataResolver) => { return mutateResolver(Mutation.Delete, dataResolver); }; export const getTypeResolver = (dataResolver: DataResolver, schema: GraphQLSchema, field: any, returnConnection = false) => { const schemaType = schema.getType(getReturnType(field.type)); let resolver; if (!isScalarType(schemaType) && !isEnumType(schemaType)) { resolver = async ( root: any, _args: { [key: string]: any }, _context: any, _info: GraphQLResolveInfo ): Promise => { const fortuneReturn = root && root.fortuneReturn ? root.fortuneReturn : root; if (!fortuneReturn) { return fortuneReturn; } const cache = root && root.cache ? root.cache : new Map(); const typeName = getReturnType(field.type); let result: any = []; let returnArray = false; let fieldValue = fortuneReturn[field.name]; returnArray = isArray(fieldValue); fieldValue = returnArray ? fieldValue : [fieldValue]; // actual value is filled from cache not just ids if (isObject(fieldValue[0])) { result = fieldValue; } const ids = []; let options = {}; _args = moveArgsIntoWhere(_args); let where = null; if (_args && _args.where) { where = _args.where; options = parseFilter(where, schemaType); } if (_args.orderBy) { set(options, 'orderBy', _args.orderBy); } if (_args.skip) { set(options, 'offset', _args.skip); } let connection: Connection; options = clean(options); // I guess use the args here instead of args as a result of cache if (!isEmpty(options)) { result = []; } if (isEmpty(result)) { fieldValue.forEach(id => { if (id) { if (cache.has(id)) { result.push(cache.get(id)); } else { ids.push(id); } } }); } let findOptions = {}; let applyOptionsWithCombinedResult = false; if (!isEmpty(result) && !isEmpty(options)) { applyOptionsWithCombinedResult = true; } else { findOptions = options; } if (!isEmpty(ids)) { let findResult = await dataResolver.find(typeName, ids, findOptions, {context: _context, info: _info}); if (findResult) { findResult = isArray(findResult) ? findResult : [findResult]; // remove null values findResult = findResult.filter(function(n) { return !!n; }); findResult.forEach(result => { cache.set(result.id, result); }); result = result.concat(findResult); } } if (applyOptionsWithCombinedResult) { result = dataResolver.applyOptions(typeName, result, options); } if ((_args.orderBy || where) && (isObjectType(schemaType) || isInterfaceType(schemaType))) { const pullIds = await filterNested(where, _args.orderBy, schemaType, fortuneReturn, cache, dataResolver, _context, _info); result = result.filter(entry => !pullIds.has(entry.id)); } // use cached data on subfields in order to support nested orderBy/where result.forEach(resultElement => { for (const resultElementField in resultElement) { if (cache.has(`${resultElement.id}.${resultElementField}`)) { resultElement[resultElementField] = cache.get(`${resultElement.id}.${resultElementField}`); } } }); connection = dataResolver.getConnection(result, _args.before, _args.after, _args.first, _args.last); result = connection.edges; result = result.map((entry) => { return { fortuneReturn: entry, cache: cache, __typename: entry.__typename }; }); result = !returnArray && result.length === 0 ? null : returnArray ? result : result[0]; if (returnConnection) { result = { edges: result, pageInfo: connection && connection.pageInfo ? connection.pageInfo : null, aggregate: connection && connection.aggregate ? connection.aggregate : null }; } return result; }; } else { resolver = async ( root: any, _args: { [key: string]: any }, _context: any, _info: GraphQLResolveInfo ): Promise => { const fortuneReturn = root && root.fortuneReturn ? root.fortuneReturn : root; const result = await defaultFieldResolver.apply(this, [fortuneReturn, _args, _context, _info]); return result; }; } return resolver; }; export const getAllResolver = (dataResolver: DataResolver, schema: GraphQLSchema, type: IntrospectionObjectType, returnConnection = false) => { return async (_root: any, _args: { [key: string]: any }, _context: any, _info: GraphQLResolveInfo) => { let options = {}; let where = null; _args = moveArgsIntoWhere(_args); const schemaType: GraphQLNamedType = schema.getType(type.name); if (_args && _args.where) { where = _args.where; options = parseFilter(_args.where, schemaType); } if (_args.orderBy) { set(options, 'orderBy', _args.orderBy); } if (_args.skip) { set(options, 'offset', _args.skip); } let connection: Connection; let result: any = []; let fortuneReturn = await dataResolver.find(type.name, null, options, {context: _context, info: _info}); if (fortuneReturn && !isEmpty(fortuneReturn)) { fortuneReturn = isArray(fortuneReturn) ? fortuneReturn : [fortuneReturn]; connection = dataResolver.getConnection(fortuneReturn, _args.before, _args.after, _args.first, _args.last); fortuneReturn = connection.edges; const cache = new Map(); fortuneReturn.forEach(result => { if (result && result.id) { cache.set(result.id, result); } }); if ((_args.orderBy || where) && (isObjectType(schemaType) || isInterfaceType(schemaType))) { const pullIds = await filterNested(where, _args.orderBy, schemaType, fortuneReturn, cache, dataResolver, _context, _info); fortuneReturn = pullIds.size > 0 ? fortuneReturn.filter(result => !pullIds.has(result ? result.id : '')) : fortuneReturn; } result = fortuneReturn.map((result) => { if (!result) { return result; } return { fortuneReturn: result, cache: cache, where, __typename: result.__typename }; }); } if (returnConnection) { result = { edges: result, pageInfo: connection && connection.pageInfo ? connection.pageInfo : null, aggregate: connection && connection.aggregate ? connection.aggregate : null }; } return result; }; }; export const queryArgs: Object = { 'first': { type: 'Int', description: 'Slice result from the start' }, 'last': { type: 'Int', description: 'Slice result from the end' }, 'skip': { type: 'Int', description: 'Skip results' }, 'before': { type: 'String', description: 'Cursor returned by previous connection queries for pagination' }, 'after': { type: 'String', description: 'Cursor returned by previous connection queries for pagination' }, }; export const fortuneFilters = ['not', 'or', 'and', 'range', 'match', 'exists']; const genieFindArgs = ['first', 'where', 'orderBy', 'local', 'last', 'skip', 'before', 'after']; export const getRootMatchFields = (matchInput: GraphQLInputObjectType): { [key: string]: GraphQLArgument } => { const matchFields = matchInput.getFields(); const args = {}; Object.keys(matchFields).forEach(key => { let newKey = key; if (genieFindArgs.includes(key)) { newKey = `f_${key}`; } args[newKey] = matchFields[key]; args[newKey].name = newKey; args[newKey].description = `${key} matches at least one of argument`; }); return args; }; export const moveArgsIntoWhere = (args: object): object => { if (!args) { return args; } Object.keys(args).forEach((argKey) => { if (!genieFindArgs.includes(argKey)) { set(args, 'where.match.' + argKey, args[argKey]); delete args[argKey]; } }); return args; }; export const parseFilter = (filter: object, type: GraphQLNamedType) => { if (!isObjectType(type) && !isInterfaceType(type)) { return filter; } if (!filter || !isObject(filter) || isArray(filter)) { return filter; } each(type.getFields(), field => { if (!fortuneFilters.includes(field.name) && filter[field.name]) { if (filter['and']) { filter['and'].push({ exists: { [field.name]: true } }); } else { set(filter, `exists.${field.name}`, true); } } }); return filter; }; export const filterNested = async (filter: object, orderBy: object, type: GraphQLNamedType, fortuneReturn: any[], cache: Map, dataResolver: DataResolver, _context, _info): Promise> => { // if they have nested filters on types we need to get that data now so we can filter at this root query const pullIds = new Set(); if (!cache) { cache = new Map(); } if ((orderBy || filter) && (isObjectType(type) || isInterfaceType(type))) { await Promise.all(map(type.getFields(), async (field) => { const currFilter = filter && filter[field.name] ? filter[field.name] : filter && filter[`f_${field.name}`] ? filter[`f_${field.name}`] : null; const currOrderBy = orderBy && orderBy[field.name] ? orderBy[field.name] : orderBy && orderBy[`f_${field.name}`] ? orderBy[`f_${field.name}`] : null; const childType = getNamedType(field.type); if (!isScalarType(childType) && !isEnumType(childType) && (currFilter || currOrderBy)) { const options = currFilter ? parseFilter(currFilter, childType) : {}; await Promise.all(fortuneReturn.map(async (result) => { if (!result) { return result; } const childIds = result[field.name]; if (childIds && !isEmpty(childIds)) { if (currOrderBy) { options['orderBy'] = currOrderBy; } let childReturn = await dataResolver.find(childType.name, childIds, options, {context: _context, info: _info}); if (isArray(childReturn) && !isEmpty(childReturn)) { const recursePullIds = await filterNested(currFilter, currOrderBy, childType, childReturn, cache, dataResolver, _context, _info); childReturn = childReturn ? childReturn.filter(result => { if (!result) { return result; } return !recursePullIds.has(result.id); }) : childReturn; } else if (childReturn && (currOrderBy || currFilter)) { const recursePullIds = await filterNested(currFilter, currOrderBy, childType, [childReturn], cache, dataResolver, _context, _info); childReturn = childReturn ? [childReturn].filter(result => { if (!result) { return result; } return !recursePullIds.has(result.id); }) : childReturn; } if (childReturn && !isEmpty(childReturn)) { if (cache) { if (childReturn.id) { cache.set(childReturn.id, childReturn); } else { cache.set(`${result.id}.${field.name}`, childReturn); } } } else { pullIds.add(result.id); } } })); } })); } return pullIds; }; export const getPayloadTypeName = (typeName: string): string => { return `${typeName}Payload`; }; export const getPayloadTypeDef = (typeName: string): string => { return ` type ${getPayloadTypeName(typeName)} { data: ${typeName} clientMutationId: String #In the case of a update or delete and you had conditions, if the conditions did not match the existing object will be returned here. data will be null unalteredData: ${typeName} }`; }; export const capFirst = (val: string) => { return val ? val.charAt(0).toUpperCase() + val.slice(1) : ''; }; export const isConnectionType = (type: GraphQLType): boolean => { let isConnection = false; if (isObjectType(type) && type.name.endsWith('Connection')) { isConnection = true; // could add more logic to not have to ban fields ending with Connection } return isConnection; }; // resolvers may have meta info that's not wanted export const getRecordFromResolverReturn = (record) => { record = record && record.fortuneReturn ? record.fortuneReturn : record; record = record && record.edges ? record.edges : record; record = record && record.node ? record.node : record; record = record && record.fortuneReturn ? record.fortuneReturn : record; record = record && record.data ? record.data : record; return record; }; export const meetsConditions = async (conditionsArgs, returnTypeName, returnType, currRecord, dataResolver, _context, _info) => { let meetsConditions = true; if (conditionsArgs) { const conditionsOptions = parseFilter(conditionsArgs, returnType); let dataAfterConditions = dataResolver.applyOptions(returnTypeName, currRecord, conditionsOptions); if (!isEmpty(dataAfterConditions)) { if (conditionsArgs && (isObjectType(returnType) || isInterfaceType(returnType))) { const pullIds = await filterNested(conditionsArgs, null, returnType, currRecord, null, dataResolver, _context, _info); dataAfterConditions = dataAfterConditions.filter(entry => !pullIds.has(entry.id)); } } if (isEmpty(dataAfterConditions)) { meetsConditions = false; } } return meetsConditions; };