import { atob , btoa } from 'abab' ; import fortune from 'fortune' ; import { IntrospectionInterfaceType , IntrospectionType } from 'graphql' ; 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 , DataResolver , DataResolverInputHook , DataResolverOutputHook , Features , FortuneOptions , FortuneRecordTypeDefinitions , GenericObject } from './GraphQLGenieInterfaces' ; import { computeRelations } from './TypeGeneratorUtilities' ; interface FortuneUpdate { id : string ; push ? : object ; replace ? : object ; pull ? : object ; } export default class FortuneGraph implements DataResolver { private fortuneOptions : FortuneOptions ; public fortuneTypeNames : Map < string , string > ; private uniqueIndexes : Map < string , string [ ] > ; private schemaInfo : IntrospectionType [ ] ; private inputHooks : Map < string , DataResolverInputHook [ ] > ; private outputHooks : Map < string , DataResolverOutputHook [ ] > ; private store ; private transaction ; constructor ( fortuneOptions , schemaInfo , fortuneRecordTypeDefinitions ? ) { this . fortuneOptions = fortuneOptions ; if ( ! this . fortuneOptions || ! this . fortuneOptions . hooks ) { set ( this . fortuneOptions , 'hooks' , { } ) ; } this . schemaInfo = schemaInfo ; this . uniqueIndexes = new Map < string , string [ ] > ( ) ; this . inputHooks = new Map < string , DataResolverInputHook [ ] > ( ) ; this . outputHooks = new Map < string , DataResolverOutputHook [ ] > ( ) ; this . store = this . buildFortune ( fortuneRecordTypeDefinitions ) ; } public computeId = ( graphType , id ? ) : string => { id = id || fortuneCommon . generateId ( ) ; return btoa ( `${ id }:${ graphType }` ) ; } ; public getTypeFromId = ( inputId ) : string => { let result ; try { result = atob ( inputId ) . split ( ':' ) [ 1 ] ; } catch ( e ) { result = inputId ; } return result ; } ; public getOriginalIdFromObjectId = ( inputId ) : string => { let result ; try { result = atob ( inputId ) . split ( ':' ) [ 0 ] ; } catch ( e ) { result = inputId ; } return result ; } ; public getValueByUnique = async ( returnTypeName , args , meta ) : Promise < Object > => { let currValue ; if ( args . id ) { currValue = await this . find ( returnTypeName , [ args . id ] , undefined , meta ) ; } else { currValue = await this . find ( returnTypeName , undefined , { match : args } , meta ) ; } return isArray ( currValue ) ? currValue [ 0 ] : currValue ; } ; public canAdd = async ( graphQLTypeName , inputRecords , meta ) : Promise < boolean > => { let canAdd = true ; if ( inputRecords && this . uniqueIndexes . has ( graphQLTypeName ) ) { await Promise . all ( this . uniqueIndexes . get ( graphQLTypeName ) . map ( async ( fieldName ) => { await Promise . all ( inputRecords . map ( async ( inputRecord ) => { if ( canAdd && inputRecord [ fieldName ] ) { const dbRecord = await this . getValueByUnique ( graphQLTypeName , { [ fieldName ] : inputRecord [ fieldName ] } , meta ) ; if ( dbRecord ) { canAdd = false ; } } } ) ) ; } ) ) ; } return canAdd ; } ; public getConnection = ( allEdges , before , after , first , last ) : Connection => { 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 ; } ; private edgesToReturn = ( edgesWithCursorApplied , first , last ) : any [ ] => { 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 ; } ; private applyCursorsToEdges = ( allEdges , before , after ) : any [ ] => { 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 ; } ; private getDataTypeName ( graphQLTypeName ) { graphQLTypeName = graphQLTypeName . endsWith ( 'Connection' ) ? graphQLTypeName . replace ( /Connection$/g , '' ) : graphQLTypeName ; graphQLTypeName = graphQLTypeName . endsWith ( 'Edge' ) ? graphQLTypeName . replace ( /Edge$/g , '' ) : graphQLTypeName ; return graphQLTypeName ; } public create = async ( graphQLTypeName , records , meta ? ) => { const fortuneType = this . getFortuneTypeName ( graphQLTypeName ) ; records [ '__typename' ] = graphQLTypeName ; if ( ! records [ 'id' ] ) { records [ 'id' ] = this . computeId ( graphQLTypeName ) ; } let results = this . transaction ? await this . transaction . create ( fortuneType , records , undefined , meta ) : await this . store . create ( fortuneType , records , undefined , meta ) ; results = results . payload . records ; return isArray ( records ) ? results : results [ 0 ] ; } ; public find = async ( graphQLTypeName , ids ? , options ? , meta ? ) => { let results ; let fortuneType ; if ( graphQLTypeName === 'Node' ) { fortuneType = this . getFortuneTypeName ( this . getTypeFromId ( ids [ 0 ] ) ) ; results = this . transaction ? await this . transaction . find ( fortuneType , ids [ 0 ] , options , undefined , meta ) : await this . store . find ( fortuneType , ids [ 0 ] , options , undefined , meta ) ; } else { fortuneType = this . getFortuneTypeName ( graphQLTypeName ) ; 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 ? await this . transaction . find ( fortuneType , ids , options , undefined , meta ) : await 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' ) ) { 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 ; } } } } graphReturn = ids && ids . length === 1 ? graphReturn [ 0 ] : graphReturn ; } if ( ! graphReturn ) { } return graphReturn ; } ; public update = async ( graphQLTypeName , records , meta ? , options ? ) => { 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 ? await this . transaction . update ( fortuneType , updates , undefined , meta ) : await this . store . update ( fortuneType , updates , undefined , meta ) ; results = results . payload . records ; return isArray ( records ) ? results : results [ 0 ] ; } ; public delete = async ( graphQLTypeName , ids ? , meta ? ) => { const fortuneType = this . getFortuneTypeName ( graphQLTypeName ) ; if ( ids . length > 0 ) { this . transaction ? await this . transaction . delete ( fortuneType , ids , undefined , meta ) : await this . store . delete ( fortuneType , ids , undefined , meta ) ; } return true ; } ; public generateUpdates = ( record , options = { } ) : FortuneUpdate => { 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 ; } ; public getLink = ( graphQLTypeName , field ) : string => { const fortuneType = this . getFortuneTypeName ( graphQLTypeName ) ; return get ( this . store , `recordTypes.${ fortuneType }.${ field }.link` ) ; } ; public getStore = ( ) => { if ( ! this . store ) { this . store = this . buildFortune ( ) ; } return this . store ; } ; private computeFortuneTypeNames = ( ) : Map < string , string > => { this . fortuneTypeNames = new Map < string , string > ( ) ; each ( keys ( this . schemaInfo ) , ( typeName ) => { if ( typeName !== 'Node' && ! this . fortuneTypeNames . has ( typeName ) ) { const type = < IntrospectionInterfaceType > 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 ; } ; private getStoreName = ( type , currStoreName , currTypeName ) : { storeName : string ; typeName : string ; } => { 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 } ; } ; public getFortuneTypeName = ( name ) : string => { name = this . getDataTypeName ( name ) ; return this . fortuneTypeNames . has ( name ) ? this . fortuneTypeNames . get ( name ) : name ; } ; private 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 ; } ; private 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 ) { case 'create' : resolve ( record ) ; break ; case 'update' : resolve ( update ) ; break ; case 'delete' : resolve ( null ) ; break ; } } ) . catch ( reason => { reject ( reason ) ; } ) ; } ) ; } ; } private 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 ) ; } ) ; } ) ; } } ; } public getFeatures ( ) { return this . store . adapter . features || { } ; } private generateOptions = ( options , graphQLTypeName ? , ids ? ) : Object => { options = options ? Object . assign ( { } , options ) : { } ; if ( graphQLTypeName && ( ! ids || ids . length < 1 ) ) { set ( options , 'match.__typename' , this . getDataTypeName ( graphQLTypeName ) ) ; } if ( has ( options , 'exists.id' ) ) { delete options . exists . id ; } 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 ; } ; public 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 ) ; } public addOutputHook ( graphQLTypeName , hook ) { if ( ! this . outputHooks . has ( graphQLTypeName ) ) { this . outputHooks . set ( graphQLTypeName , [ hook ] ) ; } else { this . outputHooks . get ( graphQLTypeName ) . push ( hook ) ; } } public addInputHook ( graphQLTypeName , hook ) { if ( ! this . inputHooks . has ( graphQLTypeName ) ) { this . inputHooks . set ( graphQLTypeName , [ hook ] ) ; } else { this . inputHooks . get ( graphQLTypeName ) . push ( hook ) ; } } public beginTransaction = async ( ) => { if ( ! this . transaction && this . store . beginTransaction ) { this . transaction = await this . store . beginTransaction ( ) ; } } ; public endTransaction = async ( ) => { if ( this . transaction && this . store . endTransaction ) { await this . store . endTransaction ( ) ; } this . transaction = null ; } ; }