UNPKG

graphql-genie

Version:
615 lines (614 loc) 32.3 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { atob, btoa } from 'abab'; import fortune from 'fortune'; import { each, find, findIndex, forOwn, get, has, isArray, isEmpty, isEqual, isPlainObject, isString, keys, merge, set, toString } from 'lodash'; import * as fortuneCommon from 'fortune/lib/adapter/adapters/common'; import { Connection } from './GraphQLGenieInterfaces'; import { computeRelations } from './TypeGeneratorUtilities'; export default class FortuneGraph { constructor(fortuneOptions, schemaInfo, fortuneRecordTypeDefinitions) { this.computeId = (graphType, id) => { id = id || fortuneCommon.generateId(); return btoa(`${id}:${graphType}`); }; this.getTypeFromId = (inputId) => { let result; try { result = atob(inputId).split(':')[1]; } catch (e) { result = inputId; } return result; }; this.getOriginalIdFromObjectId = (inputId) => { let result; try { result = atob(inputId).split(':')[0]; } catch (e) { result = inputId; } return result; }; this.getValueByUnique = (returnTypeName, args, meta) => __awaiter(this, void 0, void 0, function* () { let currValue; // tslint:disable-next-line:prefer-conditional-expression if (args.id) { currValue = yield this.find(returnTypeName, [args.id], undefined, meta); } else { currValue = yield this.find(returnTypeName, undefined, { match: args }, meta); } return isArray(currValue) ? currValue[0] : currValue; }); this.canAdd = (graphQLTypeName, inputRecords, meta) => __awaiter(this, void 0, void 0, function* () { let canAdd = true; if (inputRecords && this.uniqueIndexes.has(graphQLTypeName)) { yield Promise.all(this.uniqueIndexes.get(graphQLTypeName).map((fieldName) => __awaiter(this, void 0, void 0, function* () { yield Promise.all(inputRecords.map((inputRecord) => __awaiter(this, void 0, void 0, function* () { if (canAdd && inputRecord[fieldName]) { const dbRecord = yield this.getValueByUnique(graphQLTypeName, { [fieldName]: inputRecord[fieldName] }, meta); if (dbRecord) { canAdd = false; } } }))); }))); } return canAdd; }); this.getConnection = (allEdges, before, after, first, last) => { const connection = new Connection(); const edgesWithCursorApplied = this.applyCursorsToEdges(allEdges, before, after); connection.aggregate.count = allEdges.length; connection.edges = this.edgesToReturn(edgesWithCursorApplied, first, last); if (typeof last !== 'undefined') { if (edgesWithCursorApplied.length > last) { connection.pageInfo.hasPreviousPage = true; } } else if (typeof after !== 'undefined' && get(allEdges, '[0].id', 'id0') !== get(edgesWithCursorApplied, '[0].id', 'id1')) { connection.pageInfo.hasPreviousPage = true; } if (typeof first !== 'undefined') { if (edgesWithCursorApplied.length > first) { connection.pageInfo.hasNextPage = true; } } else if (typeof before !== 'undefined' && get(allEdges, `[${allEdges.length - 1}].id`, 'id0') !== get(edgesWithCursorApplied, `[${edgesWithCursorApplied.length - 1}].id`, 'id1')) { connection.pageInfo.hasNextPage = true; } connection.pageInfo.startCursor = get(connection.edges, '[0].id'); connection.pageInfo.endCursor = get(connection.edges, `[${connection.edges.length - 1}].id`); return connection; }; this.edgesToReturn = (edgesWithCursorApplied, first, last) => { if (typeof first !== 'undefined') { if (first < 0) { throw new Error('first must be greater than 0'); } else if (edgesWithCursorApplied.length > first) { edgesWithCursorApplied = edgesWithCursorApplied.slice(0, first); } } if (typeof last !== 'undefined') { if (last < 0) { throw new Error('first must be greater than 0'); } else if (edgesWithCursorApplied.length > last) { edgesWithCursorApplied = edgesWithCursorApplied.slice(edgesWithCursorApplied.length - last, edgesWithCursorApplied.length); } } return edgesWithCursorApplied; }; this.applyCursorsToEdges = (allEdges, before, after) => { let edges = allEdges; if (after) { const afterEdgeIndex = findIndex(edges, ['id', after]); if (afterEdgeIndex > -1) { edges = edges.slice(afterEdgeIndex + 1, edges.length); } } if (before) { const beforeEdgeIndex = findIndex(edges, ['id', before]); if (beforeEdgeIndex > -1) { edges = edges.slice(0, beforeEdgeIndex); } } return edges; }; this.create = (graphQLTypeName, records, meta) => __awaiter(this, void 0, void 0, function* () { const fortuneType = this.getFortuneTypeName(graphQLTypeName); records['__typename'] = graphQLTypeName; if (!records['id']) { records['id'] = this.computeId(graphQLTypeName); } let results = this.transaction ? yield this.transaction.create(fortuneType, records, undefined, meta) : yield this.store.create(fortuneType, records, undefined, meta); results = results.payload.records; return isArray(records) ? results : results[0]; }); this.find = (graphQLTypeName, ids, options, meta) => __awaiter(this, void 0, void 0, function* () { let results; let fortuneType; if (graphQLTypeName === 'Node') { fortuneType = this.getFortuneTypeName(this.getTypeFromId(ids[0])); results = this.transaction ? yield this.transaction.find(fortuneType, ids[0], options, undefined, meta) : yield this.store.find(fortuneType, ids[0], options, undefined, meta); } else { fortuneType = this.getFortuneTypeName(graphQLTypeName); // pull the id out of the match options if (get(options, 'match.id')) { ids = get(options, 'match.id'); delete options.match.id; } if (ids) { ids = isArray(ids) ? ids : [ids]; } options = this.generateOptions(options, graphQLTypeName, ids); results = this.transaction ? yield this.transaction.find(fortuneType, ids, options, undefined, meta) : yield this.store.find(fortuneType, ids, options, undefined, meta); } let graphReturn = get(results, 'payload.records'); if (graphReturn) { graphReturn = graphReturn.map((element) => { element.id = isString(element.id) ? element.id : toString(element.id); return element; }); if (!get(options, 'sort')) { // to support relay plural identifying root fields results should be in the order in which they were requested, // with null being returned by failed finds if (ids) { const newReturn = []; ids.forEach((value) => { const foundResult = find(graphReturn, ['id', value]); if (foundResult) { newReturn.push(foundResult); } else { newReturn.push(null); } }); graphReturn = newReturn; } else if (this.uniqueIndexes.has(graphQLTypeName)) { const matches = options.match; if (matches['__typename']) { delete matches['__typename']; } const matchesKeys = Object.keys(matches); if (matchesKeys.length === 1) { const key = matchesKeys[0]; if (this.uniqueIndexes.get(graphQLTypeName).includes(key)) { const theMatch = isArray(matches[key]) ? matches[key] : [matches[key]]; const newReturn = []; theMatch.forEach((value) => { const foundResult = find(graphReturn, [key, value]); if (foundResult) { newReturn.push(foundResult); } else { newReturn.push(null); } }); graphReturn = newReturn; } } } } // if one id sent in we just want to return the value not an array graphReturn = ids && ids.length === 1 ? graphReturn[0] : graphReturn; } if (!graphReturn) { // console.log('Nothing Found ' + graphQLTypeName + ' ' + JSON.stringify(ids) + ' ' + JSON.stringify(options)); } return graphReturn; }); this.update = (graphQLTypeName, records, meta, options) => __awaiter(this, void 0, void 0, function* () { const fortuneType = this.getFortuneTypeName(graphQLTypeName); let updates = records; if (!options || !options['fortuneFormatted']) { updates = isArray(records) ? records.map(value => this.generateUpdates(value, options)) : this.generateUpdates(records, options); } let results = this.transaction ? yield this.transaction.update(fortuneType, updates, undefined, meta) : yield this.store.update(fortuneType, updates, undefined, meta); results = results.payload.records; return isArray(records) ? results : results[0]; }); this.delete = (graphQLTypeName, ids, meta) => __awaiter(this, void 0, void 0, function* () { const fortuneType = this.getFortuneTypeName(graphQLTypeName); if (ids.length > 0) { this.transaction ? yield this.transaction.delete(fortuneType, ids, undefined, meta) : yield this.store.delete(fortuneType, ids, undefined, meta); } return true; }); this.generateUpdates = (record, options = {}) => { const updates = { id: record['id'] }; for (const argName in record) { const arg = record[argName]; if (argName !== 'id') { if (isArray(arg)) { if (options['pull']) { updates.pull = updates.pull || {}; updates.pull[argName] = arg; } else { updates.push = updates.push || {}; updates.push[argName] = arg; } } else if (isPlainObject(arg)) { if (arg.push) { updates.push = updates.push || {}; updates.push[argName] = arg.push; } if (arg.pull) { updates.pull = updates.pull || {}; updates.pull[argName] = arg.pull; } if (arg.set) { updates.replace = updates.replace || {}; updates.replace[argName] = arg.set; } } else { updates.replace = updates.replace || {}; updates.replace[argName] = arg; } } } return updates; }; this.getLink = (graphQLTypeName, field) => { const fortuneType = this.getFortuneTypeName(graphQLTypeName); return get(this.store, `recordTypes.${fortuneType}.${field}.link`); }; this.getStore = () => { if (!this.store) { this.store = this.buildFortune(); } return this.store; }; this.computeFortuneTypeNames = () => { this.fortuneTypeNames = new Map(); each(keys(this.schemaInfo), (typeName) => { if (typeName !== 'Node' && !this.fortuneTypeNames.has(typeName)) { const type = this.schemaInfo[typeName]; let storeObj = this.getStoreName(type, null, null); let storeName = storeObj.storeName; let storeTypeName = storeObj.typeName; if (!isEmpty(type.possibleTypes)) { const possibleTypes = [type.name]; each(type.possibleTypes, possibleType => { if (possibleTypes.indexOf(possibleType.name) < 0) { possibleTypes.push(possibleType.name); } possibleType = this.schemaInfo[possibleType.name]; storeObj = this.getStoreName(possibleType, storeName, storeTypeName); storeName = storeObj.storeName; storeTypeName = storeObj.typeName; each(possibleType['interfaces'], currInterface => { currInterface = this.schemaInfo[currInterface.name]; if (currInterface.name !== 'Node' && currInterface.name !== storeTypeName) { storeObj = this.getStoreName(currInterface, storeName, storeTypeName); storeName = storeObj.storeName; storeTypeName = storeObj.typeName; if (possibleTypes.indexOf(currInterface.name) < 0) { possibleTypes.push(currInterface.name); } if (!isEmpty(currInterface.possibleTypes)) { each(currInterface.possibleTypes, currInterfacePossibleType => { if (possibleTypes.indexOf(currInterfacePossibleType.name) < 0) { currInterfacePossibleType = this.schemaInfo[currInterfacePossibleType.name]; storeObj = this.getStoreName(currInterfacePossibleType, storeName, storeTypeName); storeName = storeObj.storeName; storeTypeName = storeObj.typeName; possibleTypes.push(currInterfacePossibleType.name); } }); } } }); each(possibleType['unions'], currUnion => { currUnion = this.schemaInfo[currUnion.name]; if (currUnion.name !== storeTypeName) { if (possibleTypes.indexOf(currUnion.name) < 0) { storeObj = this.getStoreName(currUnion, storeName, storeTypeName); storeName = storeObj.storeName; storeTypeName = storeObj.typeName; possibleTypes.push(currUnion.name); } if (!isEmpty(currUnion.possibleTypes)) { each(currUnion.possibleTypes, currUnionPossibleType => { if (possibleTypes.indexOf(currUnionPossibleType.name) < 0) { currUnionPossibleType = this.schemaInfo[currUnionPossibleType.name]; storeObj = this.getStoreName(currUnionPossibleType, storeName, storeTypeName); storeName = storeObj.storeName; storeTypeName = storeObj.typeName; possibleTypes.push(currUnionPossibleType.name); } }); } } }); }); const fortuneTypeName = storeName || (possibleTypes.sort() && possibleTypes.join('_')); each(possibleTypes, currTypeName => { this.fortuneTypeNames.set(currTypeName, fortuneTypeName); }); } else if (!isEmpty(storeName)) { this.fortuneTypeNames.set(typeName, storeName); } } }); return this.fortuneTypeNames; }; this.getStoreName = (type, currStoreName, currTypeName) => { let storeName = get(type, 'metadata.storeName', null); let typeName = type.name; if (!isEmpty(storeName) && currStoreName && currStoreName !== storeName) { throw new Error(`Conflictiing store names which need to be the same ${storeName} on ${typeName} does not match ${currStoreName} on ${currTypeName} `); } typeName = !isEmpty(storeName) ? typeName : currTypeName; storeName = !isEmpty(storeName) ? storeName : currStoreName; return { storeName, typeName }; }; this.getFortuneTypeName = (name) => { name = this.getDataTypeName(name); return this.fortuneTypeNames.has(name) ? this.fortuneTypeNames.get(name) : name; }; this.buildFortune = (fortuneRecordTypeDefinitions) => { this.computeFortuneTypeNames(); const relations = computeRelations(this.schemaInfo, this.getFortuneTypeName); const fortuneConfig = {}; forOwn(this.schemaInfo, (type, name) => { if (type.kind === 'OBJECT' && name !== 'Query' && name !== 'Mutation' && name !== 'Subscription') { const fields = {}; forOwn(type.fields, (field) => { if (field.name !== 'id') { let currType = field.type; let isArray = false; while (currType.kind === 'NON_NULL' || currType.kind === 'LIST') { if (currType.kind === 'LIST') { isArray = true; } currType = currType.ofType; } if (get(field, 'metadata.unique') === true) { if (isArray) { console.error('Unique may not work on list types', name, field.name); } if (!this.uniqueIndexes.has(name)) { this.uniqueIndexes.set(name, []); } this.uniqueIndexes.get(name).push(field.name); } if (get(field, 'metadata.createdTimestamp') === true) { this.addInputHook(name, (context, record) => { switch (context.request.method) { case 'create': if (!record.hasOwnProperty(field.name)) { record[field.name] = new Date(); } return record; } }); } if (get(field, 'metadata.updatedTimestamp') === true) { this.addInputHook(name, (context, _record, update) => { switch (context.request.method) { case 'update': if (!('replace' in update)) update.replace = {}; if (!update.replace.hasOwnProperty(field.name)) { update.replace[field.name] = new Date(); } return update; } }); } const currKind = currType.kind; currType = currType.kind === 'ENUM' ? 'String' : currType.name; if (currType === 'ID' || currType === 'String') { currType = String; } else if (currType === 'Int' || currType === 'Float') { currType = Number; } else if (currType === 'Boolean') { currType = Boolean; } else if (currType === 'JSON') { currType = Object; } else if (currType === 'Date' || currType === 'Time' || currType === 'DateTime') { currType = Date; } else if (currKind === 'SCALAR') { currType = Object; } let inverse; if (isString(currType)) { currType = this.getFortuneTypeName(currType); inverse = relations.getInverseWithoutName(currType, field.name, this.getFortuneTypeName(type.name)); } currType = isArray ? Array(currType) : currType; if (inverse) { currType = [currType, inverse]; } fields[field.name] = currType; } fields.__typename = String; }); const fortuneName = this.getFortuneTypeName(name); const fortuneConfigForName = fortuneConfig[fortuneName] ? fortuneConfig[fortuneName] : {}; each(keys(fields), (fieldName) => { const currType = fortuneConfigForName[fieldName]; const newType = fields[fieldName]; if (!currType) { fortuneConfigForName[fieldName] = newType; } else { let badSchema = typeof newType !== typeof currType; badSchema = badSchema ? badSchema : !isEqual(fortuneConfigForName[fieldName], fields[fieldName]); if (badSchema) { console.error('Bad schema. Types that share unions/interfaces have fields of the same name but different types. This is not allowed\n', 'fortune type', fortuneName, '\n', 'field name', fieldName, '\n', 'currType', fortuneConfigForName[fieldName], '\n', 'newType', fields[fieldName]); } } }); fortuneConfig[fortuneName] = fortuneConfigForName; if (!this.fortuneOptions.hooks[fortuneName]) { this.fortuneOptions.hooks[fortuneName] = [this.inputHook(fortuneName), this.outputHook(fortuneName)]; } } }); if (fortuneRecordTypeDefinitions) { merge(fortuneConfig, fortuneRecordTypeDefinitions); } const store = fortune(fortuneConfig, this.fortuneOptions); return store; }; this.generateOptions = (options, graphQLTypeName, ids) => { options = options ? Object.assign({}, options) : {}; // if no ids we need to make sure we only get the necessary typename so that this works with interfaces/unions if (graphQLTypeName && (!ids || ids.length < 1)) { set(options, 'match.__typename', this.getDataTypeName(graphQLTypeName)); } if (has(options, 'exists.id')) { delete options.exists.id; } // make sure sort is boolean rather than enum if (options.orderBy) { const sort = {}; for (const fieldName in options.orderBy) { if (options.orderBy[fieldName] === 'ASCENDING' || options.orderBy[fieldName] === 'ASC') { sort[fieldName] = true; } else if (options.orderBy[fieldName] === 'DESCENDING' || options.orderBy[fieldName] === 'DESC') { sort[fieldName] = false; } } options.sort = sort; delete options.orderBy; } return options; }; this.beginTransaction = () => __awaiter(this, void 0, void 0, function* () { if (!this.transaction && this.store.beginTransaction) { this.transaction = yield this.store.beginTransaction(); } }); this.endTransaction = () => __awaiter(this, void 0, void 0, function* () { if (this.transaction && this.store.endTransaction) { yield this.store.endTransaction(); } this.transaction = null; }); this.fortuneOptions = fortuneOptions; if (!this.fortuneOptions || !this.fortuneOptions.hooks) { set(this.fortuneOptions, 'hooks', {}); } this.schemaInfo = schemaInfo; this.uniqueIndexes = new Map(); this.inputHooks = new Map(); this.outputHooks = new Map(); this.store = this.buildFortune(fortuneRecordTypeDefinitions); } getDataTypeName(graphQLTypeName) { graphQLTypeName = graphQLTypeName.endsWith('Connection') ? graphQLTypeName.replace(/Connection$/g, '') : graphQLTypeName; graphQLTypeName = graphQLTypeName.endsWith('Edge') ? graphQLTypeName.replace(/Edge$/g, '') : graphQLTypeName; return graphQLTypeName; } inputHook(_fortuneType) { return (context, record, update) => { const promises = []; if (record['__typename'] && this.inputHooks.has(record['__typename'])) { this.inputHooks.get(record['__typename']).forEach(hook => { const returnVal = hook(context, record, update); if (returnVal instanceof Promise) { promises.push(returnVal); } }); } return new Promise((resolve, reject) => { Promise.all(promises).then(() => { switch (context.request.method) { // If it's a create request, return the record. case 'create': resolve(record); break; // If it's an update request, return the update. case 'update': resolve(update); break; // If it's a delete request, the return value doesn't matter. case 'delete': resolve(null); break; } }).catch(reason => { reject(reason); }); }); }; } outputHook(_fortuneType) { return (context, record) => { if (record['__typename'] && this.outputHooks.has(record['__typename'])) { const promises = []; this.outputHooks.get(record['__typename']).forEach(hook => { const returnVal = hook(context, record); if (returnVal instanceof Promise) { promises.push(returnVal); } }); return new Promise((resolve, reject) => { Promise.all(promises).then(() => { resolve(record); }).catch(reason => { reject(reason); }); }); } }; } getFeatures() { return this.store.adapter.features || {}; } applyOptions(graphQLTypeName, records, options, meta) { records = isArray(records) ? records : [records]; options = this.generateOptions(options); const fortuneType = this.getFortuneTypeName(graphQLTypeName); return fortuneCommon.applyOptions(this.store.recordTypes[fortuneType], records, options, meta); } addOutputHook(graphQLTypeName, hook) { if (!this.outputHooks.has(graphQLTypeName)) { this.outputHooks.set(graphQLTypeName, [hook]); } else { this.outputHooks.get(graphQLTypeName).push(hook); } } addInputHook(graphQLTypeName, hook) { if (!this.inputHooks.has(graphQLTypeName)) { this.inputHooks.set(graphQLTypeName, [hook]); } else { this.inputHooks.get(graphQLTypeName).push(hook); } } }