import { ApolloClient , HttpLink , InMemoryCache , NormalizedCacheObject , gql , } from '@apollo/client/core' ; import { Injectable , OnApplicationShutdown } from '@nestjs/common' ; import { buildQuery , GqlNode , GqlQuery , GqlVar , MetaData } from '@subql/common' ; import { DictionaryQueryCondition , DictionaryQueryEntry } from '@subql/types' ; import fetch from 'node-fetch' ; import { SubqueryProject } from '../configure/project.model' ; import { getLogger } from '../utils/logger' ; import { profiler } from '../utils/profiler' ; import { getYargsOption } from '../yargs' ; export type Dictionary = { _metadata : MetaData ; batchBlocks : number [ ] ; } ; const logger = getLogger ( 'dictionary' ) ; const { argv } = getYargsOption ( ) ; function extractVar ( name , cond ) { return { name , gqlType : 'String!' , value : cond . value , } ; } const ARG_FIELD_REGX = /[^a-zA-Z0-9-_]/g ; function sanitizeArgField ( input ) { return input . replace ( ARG_FIELD_REGX , '' ) ; } function extractVars ( entity , conditions ) { const gqlVars = [ ] ; const filter = { or : [ ] } ; conditions . forEach ( ( i , outerIdx ) => { if ( i . length > 1 ) { filter . or [ outerIdx ] = { and : i . map ( ( j , innerIdx ) => { const v = extractVar ( `${ entity }_${ outerIdx }_${ innerIdx }` , j ) ; gqlVars . push ( v ) ; return { [ sanitizeArgField ( j . field ) ] : { equalTo : `$${ v . name }` } } ; } ) , } ; } else if ( i . length === 1 ) { const v = extractVar ( `${ entity }_${ outerIdx }_0` , i [ 0 ] ) ; gqlVars . push ( v ) ; filter . or [ outerIdx ] = { [ sanitizeArgField ( i [ 0 ] . field ) ] : { equalTo : `$${ v . name }` } , } ; } } ) ; return [ gqlVars , filter ] ; } function buildDictQueryFragment ( entity , startBlock , queryEndBlock , conditions , batchSize ) { const [ gqlVars , filter ] = extractVars ( entity , conditions ) ; const node = { entity , project : [ { entity : 'nodes' , project : [ 'blockHeight' ] , } , ] , args : { filter : { ... filter , blockHeight : { greaterThanOrEqualTo : `"${ startBlock }"` , lessThan : `"${ queryEndBlock }"` , } , } , orderBy : 'BLOCK_HEIGHT_ASC' , first : batchSize . toString ( ) , } , } ; return [ gqlVars , node ] ; } @ Injectable ( ) export class DictionaryService implements OnApplicationShutdown { private client : ApolloClient < NormalizedCacheObject > ; private isShutdown = false ; constructor ( protected project ) { this . client = new ApolloClient ( { cache : new InMemoryCache ( { resultCaching : true } ) , link : new HttpLink ( { uri : this . project . network . dictionary , fetch } ) , defaultOptions : { watchQuery : { fetchPolicy : 'no-cache' , } , query : { fetchPolicy : 'no-cache' , } , } , } ) ; } onApplicationShutdown ( ) { this . isShutdown = true ; } param startBlock param queryEndBlock param batchSize param conditions @ profiler ( argv . profiler ) async getDictionary ( startBlock , queryEndBlock , batchSize , conditions ) { const { query , variables } = this . dictionaryQuery ( startBlock , queryEndBlock , batchSize , conditions ) ; try { const resp = await this . client . query ( { query : gql ( query ) , variables , } ) ; const blockHeightSet = new Set < number > ( ) ; const specVersionBlockHeightSet = new Set < number > ( ) ; const entityEndBlock = { } ; for ( const entity of Object . keys ( resp . data ) ) { if ( entity !== 'specVersions' && entity !== '_metadata' && resp . data [ entity ] . nodes . length >= 0 ) { for ( const node of resp . data [ entity ] . nodes ) { blockHeightSet . add ( Number ( node . blockHeight ) ) ; entityEndBlock [ entity ] = Number ( node . blockHeight ) ; } } } if ( resp . data . specVersions && resp . data . specVersions . nodes . length >= 0 ) { for ( const node of resp . data . specVersions . nodes ) { specVersionBlockHeightSet . add ( Number ( node . blockHeight ) ) ; } } const _metadata = resp . data . _metadata ; const endBlock = Math . min ( ... Object . values ( entityEndBlock ) . map ( ( height ) => isNaN ( height ) ? Infinity : height ) ) ; const batchBlocks = Array . from ( blockHeightSet ) . filter ( ( block ) => block <= endBlock ) . sort ( ( n1 , n2 ) => n1 - n2 ) ; return { _metadata , batchBlocks , } ; } catch ( err ) { logger . warn ( err , `failed to fetch dictionary result` ) ; return undefined ; } } private dictionaryQuery ( startBlock , queryEndBlock , batchSize , conditions ) { const mapped = conditions . reduce ( ( acc , c ) => { acc [ c . entity ] = acc [ c . entity ] || [ ] ; acc [ c . entity ] . push ( c . conditions ) ; return acc ; } , { } ) ; const vars = [ ] ; const nodes = [ { entity : '_metadata' , project : [ 'lastProcessedHeight' , 'genesisHash' ] , } , { entity : 'specVersions' , project : [ { entity : 'nodes' , project : [ 'id' , 'blockHeight' ] , } , ] , } , ] ; for ( const entity of Object . keys ( mapped ) ) { const [ pVars , node ] = buildDictQueryFragment ( entity , startBlock , queryEndBlock , mapped [ entity ] , batchSize ) ; nodes . push ( node ) ; vars . push ( ... pVars ) ; } return buildQuery ( vars , nodes ) ; } }