import argon2 from 'argon2' ; import { validateQuery } from '../utils/validate-query' ; import { ArgumentNode , BooleanValueNode , execute , ExecutionResult , FieldNode , formatError , FormattedExecutionResult , FragmentDefinitionNode , GraphQLBoolean , GraphQLEnumType , GraphQLError , GraphQLFloat , GraphQLID , GraphQLInt , GraphQLList , GraphQLNonNull , GraphQLNullableType , GraphQLObjectType , GraphQLResolveInfo , GraphQLScalarType , GraphQLSchema , GraphQLString , GraphQLUnionType , InlineFragmentNode , IntValueNode , ObjectFieldNode , ObjectValueNode , SelectionNode , specifiedRules , StringValueNode , validate , } from 'graphql' ; import { Filter } from '@directus/shared/types' ; import { GraphQLJSON , InputTypeComposer , InputTypeComposerFieldConfigMapDefinition , ObjectTypeComposer , ObjectTypeComposerFieldConfigMapDefinition , SchemaComposer , toInputObjectType , } from 'graphql-compose' ; import { Knex } from 'knex' ; import { flatten , get , mapKeys , merge , set , uniq , pick , transform , isObject , omit } from 'lodash' ; import ms from 'ms' ; import { getCache } from '../cache' ; import getDatabase from '../database' ; import env from '../env' ; import { BaseException } from '@directus/shared/exceptions' ; import { ForbiddenException , GraphQLValidationException , InvalidPayloadException } from '../exceptions' ; import { getExtensionManager } from '../extensions' ; import { Accountability , Query , Aggregate } from '@directus/shared/types' ; import { AbstractServiceOptions , Action , GraphQLParams , Item , SchemaOverview } from '../types' ; import { getGraphQLType } from '../utils/get-graphql-type' ; import { reduceSchema } from '../utils/reduce-schema' ; import { sanitizeQuery } from '../utils/sanitize-query' ; import { ActivityService } from './activity' ; import { AuthenticationService } from './authentication' ; import { CollectionsService } from './collections' ; import { FieldsService } from './fields' ; import { FilesService } from './files' ; import { FoldersService } from './folders' ; import { ItemsService } from './items' ; import { PermissionsService } from './permissions' ; import { PresetsService } from './presets' ; import { NotificationsService } from './notifications' ; import { RelationsService } from './relations' ; import { RevisionsService } from './revisions' ; import { RolesService } from './roles' ; import { ServerService } from './server' ; import { SettingsService } from './settings' ; import { SharesService } from './shares' ; import { SpecificationService } from './specifications' ; import { TFAService } from './tfa' ; import { UsersService } from './users' ; import { UtilsService } from './utils' ; import { WebhooksService } from './webhooks' ; import { generateHash } from '../utils/generate-hash' ; import { DEFAULT_AUTH_PROVIDER } from '../constants' ; const GraphQLVoid = new GraphQLScalarType ( { name : 'Void' , description : 'Represents NULL values' , serialize ( ) { return null ; } , parseValue ( ) { return null ; } , parseLiteral ( ) { return null ; } , } ) ; export const GraphQLGeoJSON = new GraphQLScalarType ( { ... GraphQLJSON , name : 'GraphQLGeoJSON' , description : 'GeoJSON value' , } ) ; export const GraphQLDate = new GraphQLScalarType ( { ... GraphQLString , name : 'Date' , description : 'ISO8601 Date values' , } ) ; /**
 * These should be ignored in the context of GraphQL, and/or are replaced by a custom resolver (for non-standard structures)
 */ const SYSTEM_DENY_LIST = [ 'directus_collections' , 'directus_fields' , 'directus_relations' , 'directus_migrations' , 'directus_sessions' , ] ; const READ_ONLY = [ 'directus_activity' , 'directus_revisions' ] ; export class GraphQLService { accountability : Accountability | null ; knex : Knex ; schema : SchemaOverview ; scope : 'items' | 'system' ; constructor ( options ) { this . accountability = options ?. accountability || null ; this . knex = options ?. knex || getDatabase ( ) ; this . schema = options . schema ; this . scope = options . scope ; } /**
     * Execute a GraphQL structure
     */ async execute ( { document , variables , operationName , contextValue , } ) { const schema = this . getSchema ( ) ; const validationErrors = validate ( schema , document , specifiedRules ) ; if ( validationErrors . length > 0 ) { throw new GraphQLValidationException ( { graphqlErrors : validationErrors } ) ; } let result ; try { result = await execute ( { schema , document , contextValue , variableValues : variables , operationName , } ) ; } catch ( err ) { throw new InvalidPayloadException ( 'GraphQL execution error.' , { graphqlErrors : [ err . message ] } ) ; } const formattedResult = { ... result , errors : result . errors ?. map ( formatError ) , } ; return formattedResult ; } /**
     * Generate the GraphQL schema. Pulls from the schema information generated by the get-schema util.
     */ getSchema ( ) ; getSchema ( type ) ; getSchema ( type ) ; getSchema ( type = 'schema' ) { const self = this ; const schemaComposer = new SchemaComposer < GraphQLParams [ 'contextValue' ] > ( ) ; const schema = { read : this . accountability ?. admin === true ? this . schema : reduceSchema ( this . schema , this . accountability ?. permissions || null , [ 'read' ] ) , create : this . accountability ?. admin === true ? this . schema : reduceSchema ( this . schema , this . accountability ?. permissions || null , [ 'create' ] ) , update : this . accountability ?. admin === true ? this . schema : reduceSchema ( this . schema , this . accountability ?. permissions || null , [ 'update' ] ) , delete : this . accountability ?. admin === true ? this . schema : reduceSchema ( this . schema , this . accountability ?. permissions || null , [ 'delete' ] ) , } ; const { ReadCollectionTypes } = getReadableTypes ( ) ; const { CreateCollectionTypes , UpdateCollectionTypes , DeleteCollectionTypes } = getWritableTypes ( ) ; const scopeFilter = ( collection ) => { if ( this . scope === 'items' && collection . collection . startsWith ( 'directus_' ) === true ) return false ; if ( this . scope === 'system' ) { if ( collection . collection . startsWith ( 'directus_' ) === false ) return false ; if ( SYSTEM_DENY_LIST . includes ( collection . collection ) ) return false ; } return true ; } ; if ( this . scope === 'system' ) { this . injectSystemResolvers ( schemaComposer , { CreateCollectionTypes , ReadCollectionTypes , UpdateCollectionTypes , DeleteCollectionTypes , } , schema ) ; } const readableCollections = Object . values ( schema . read . collections ) . filter ( ( collection ) => collection . collection in ReadCollectionTypes ) . filter ( scopeFilter ) ; if ( readableCollections . length > 0 ) { schemaComposer . Query . addFields ( readableCollections . reduce ( ( acc , collection ) => { const collectionName = this . scope === 'items' ? collection . collection : collection . collection . substring ( 9 ) ; acc [ collectionName ] = ReadCollectionTypes [ collection . collection ] . getResolver ( collection . collection ) ; if ( this . schema . collections [ collection . collection ] . singleton === false ) { acc [ `${ collectionName }_by_id` ] = ReadCollectionTypes [ collection . collection ] . getResolver ( `${ collection . collection }_by_id` ) ; const hasAggregate = Object . values ( collection . fields ) . some ( ( field ) => { const graphqlType = getGraphQLType ( field . type ) ; if ( graphqlType === GraphQLInt || graphqlType === GraphQLFloat ) { return true ; } return false ; } ) ; if ( hasAggregate ) { acc [ `${ collectionName }_aggregated` ] = ReadCollectionTypes [ collection . collection ] . getResolver ( `${ collection . collection }_aggregated` ) ; } } return acc ; } , { } as ObjectTypeComposerFieldConfigMapDefinition < any , any > ) ) ; } else { schemaComposer . Query . addFields ( { _empty : { type : GraphQLVoid , description : "There's no data to query." , } , } ) ; } if ( Object . keys ( schema . create . collections ) . length > 0 ) { schemaComposer . Mutation . addFields ( Object . values ( schema . create . collections ) . filter ( ( collection ) => collection . collection in CreateCollectionTypes && collection . singleton === false ) . filter ( scopeFilter ) . filter ( ( collection ) => READ_ONLY . includes ( collection . collection ) === false ) . reduce ( ( acc , collection ) => { const collectionName = this . scope === 'items' ? collection . collection : collection . collection . substring ( 9 ) ; acc [ `create_${ collectionName }_items` ] = CreateCollectionTypes [ collection . collection ] . getResolver ( `create_${ collection . collection }_items` ) ; acc [ `create_${ collectionName }_item` ] = CreateCollectionTypes [ collection . collection ] . getResolver ( `create_${ collection . collection }_item` ) ; return acc ; } , { } as ObjectTypeComposerFieldConfigMapDefinition < any , any > ) ) ; } if ( Object . keys ( schema . update . collections ) . length > 0 ) { schemaComposer . Mutation . addFields ( Object . values ( schema . update . collections ) . filter ( ( collection ) => collection . collection in UpdateCollectionTypes ) . filter ( scopeFilter ) . filter ( ( collection ) => READ_ONLY . includes ( collection . collection ) === false ) . reduce ( ( acc , collection ) => { const collectionName = this . scope === 'items' ? collection . collection : collection . collection . substring ( 9 ) ; if ( collection . singleton ) { acc [ `update_${ collectionName }` ] = UpdateCollectionTypes [ collection . collection ] . getResolver ( `update_${ collection . collection }` ) ; } else { acc [ `update_${ collectionName }_items` ] = UpdateCollectionTypes [ collection . collection ] . getResolver ( `update_${ collection . collection }_items` ) ; acc [ `update_${ collectionName }_item` ] = UpdateCollectionTypes [ collection . collection ] . getResolver ( `update_${ collection . collection }_item` ) ; } return acc ; } , { } as ObjectTypeComposerFieldConfigMapDefinition < any , any > ) ) ; } if ( Object . keys ( schema . delete . collections ) . length > 0 ) { schemaComposer . Mutation . addFields ( Object . values ( schema . delete . collections ) . filter ( ( collection ) => collection . singleton === false ) . filter ( scopeFilter ) . filter ( ( collection ) => READ_ONLY . includes ( collection . collection ) === false ) . reduce ( ( acc , collection ) => { const collectionName = this . scope === 'items' ? collection . collection : collection . collection . substring ( 9 ) ; acc [ `delete_${ collectionName }_items` ] = DeleteCollectionTypes . many . getResolver ( `delete_${ collection . collection }_items` ) ; acc [ `delete_${ collectionName }_item` ] = DeleteCollectionTypes . one . getResolver ( `delete_${ collection . collection }_item` ) ; return acc ; } , { } as ObjectTypeComposerFieldConfigMapDefinition < any , any > ) ) ; } if ( type === 'sdl' ) { return schemaComposer . toSDL ( ) ; } return schemaComposer . buildSchema ( ) ; /**
         * Construct an object of types for every collection, using the permitted fields per action type
         * as it's fields.
         */ function getTypes ( action ) { const CollectionTypes = { } ; const DateFunctions = schemaComposer . createObjectTC ( { name : 'date_functions' , fields : { year : { type : GraphQLInt , } , month : { type : GraphQLInt , } , week : { type : GraphQLInt , } , day : { type : GraphQLInt , } , weekday : { type : GraphQLInt , } , } , } ) ; const TimeFunctions = schemaComposer . createObjectTC ( { name : 'time_functions' , fields : { hour : { type : GraphQLInt , } , minute : { type : GraphQLInt , } , second : { type : GraphQLInt , } , } , } ) ; const DateTimeFunctions = schemaComposer . createObjectTC ( { name : 'datetime_functions' , fields : { ... DateFunctions . getFields ( ) , ... TimeFunctions . getFields ( ) , } , } ) ; for ( const collection of Object . values ( schema [ action ] . collections ) ) { if ( Object . keys ( collection . fields ) . length === 0 ) continue ; if ( SYSTEM_DENY_LIST . includes ( collection . collection ) ) continue ; CollectionTypes [ collection . collection ] = schemaComposer . createObjectTC ( { name : action === 'read' ? collection . collection : `${ action }_${ collection . collection }` , fields : Object . values ( collection . fields ) . reduce ( ( acc , field ) => { let type = getGraphQLType ( field . type ) ; if ( field . nullable === false && action !== 'update' ) { type = GraphQLNonNull ( type ) ; } if ( collection . primary === field . field ) { type = GraphQLID ; } acc [ field . field ] = { type , description : field . note , resolve : ( obj ) => { return obj [ field . field ] ; } , } ; if ( field . type === 'date' ) { acc [ `${ field . field }_func` ] = { type : DateFunctions , resolve : ( obj ) => { const funcFields = Object . keys ( DateFunctions . getFields ( ) ) . map ( ( key ) => `${ field . field }_${ key }` ) ; return mapKeys ( pick ( obj , funcFields ) , ( _value , key ) => key . substring ( field . field . length + 1 ) ) ; } , } ; } if ( field . type === 'time' ) { acc [ `${ field . field }_func` ] = { type : TimeFunctions , resolve : ( obj ) => { const funcFields = Object . keys ( TimeFunctions . getFields ( ) ) . map ( ( key ) => `${ field . field }_${ key }` ) ; return mapKeys ( pick ( obj , funcFields ) , ( _value , key ) => key . substring ( field . field . length + 1 ) ) ; } , } ; } if ( field . type === 'dateTime' || field . type === 'timestamp' ) { acc [ `${ field . field }_func` ] = { type : DateTimeFunctions , resolve : ( obj ) => { const funcFields = Object . keys ( DateTimeFunctions . getFields ( ) ) . map ( ( key ) => `${ field . field }_${ key }` ) ; return mapKeys ( pick ( obj , funcFields ) , ( _value , key ) => key . substring ( field . field . length + 1 ) ) ; } , } ; } return acc ; } , { } as ObjectTypeComposerFieldConfigMapDefinition < any , any > ) , } ) ; } for ( const relation of schema [ action ] . relations ) { if ( relation . related_collection ) { if ( SYSTEM_DENY_LIST . includes ( relation . related_collection ) ) continue ; CollectionTypes [ relation . collection ] ?. addFields ( { [ relation . field ] : { type : CollectionTypes [ relation . related_collection ] , resolve : ( obj , _ , __ , info ) => { return obj [ info ?. path ?. key ?? relation . field ] ; } , } , } ) ; if ( relation . meta ?. one_field ) { CollectionTypes [ relation . related_collection ] ?. addFields ( { [ relation . meta . one_field ] : { type : [ CollectionTypes [ relation . collection ] ] , resolve : ( obj , _ , __ , info ) => { return obj [ info ?. path ?. key ?? relation . meta ! . one_field ] ; } , } , } ) ; } } else if ( relation . meta ?. one_allowed_collections && action === 'read' ) { CollectionTypes [ relation . collection ] ?. addFields ( { [ relation . field ] : { type : new GraphQLUnionType ( { name : `${ relation . collection }_${ relation . field }_union` , types : relation . meta . one_allowed_collections . map ( ( collection ) => CollectionTypes [ collection ] . getType ( ) ) , resolveType ( value , context , info ) { let path = [ ] ; let currentPath = info . path ; while ( currentPath . prev ) { path . push ( currentPath . key ) ; currentPath = currentPath . prev ; } path = path . reverse ( ) . slice ( 0 , - 1 ) ; let parent = context . data ; for ( const pathPart of path ) { parent = parent [ pathPart ] ; } const collection = parent [ relation . meta ! . one_collection_field ! ] ; return CollectionTypes [ collection ] . getType ( ) ; } , } ) , resolve : ( obj , _ , __ , info ) => { return obj [ info ?. path ?. key ?? relation . field ] ; } , } , } ) ; } } return { CollectionTypes } ; } /**
         * Create readable types and attach resolvers for each. Also prepares full filter argument structures
         */ function getReadableTypes ( ) { const { CollectionTypes : ReadCollectionTypes } = getTypes ( 'read' ) ; const ReadableCollectionFilterTypes = { } ; const AggregatedFunctions = { } ; const AggregatedFilters = { } ; const StringFilterOperators = schemaComposer . createInputTC ( { name : 'string_filter_operators' , fields : { _eq : { type : GraphQLString , } , _neq : { type : GraphQLString , } , _contains : { type : GraphQLString , } , _ncontains : { type : GraphQLString , } , _starts_with : { type : GraphQLString , } , _nstarts_with : { type : GraphQLString , } , _ends_with : { type : GraphQLString , } , _nends_with : { type : GraphQLString , } , _in : { type : new GraphQLList ( GraphQLString ) , } , _nin : { type : new GraphQLList ( GraphQLString ) , } , _null : { type : GraphQLBoolean , } , _nnull : { type : GraphQLBoolean , } , _empty : { type : GraphQLBoolean , } , _nempty : { type : GraphQLBoolean , } , } , } ) ; const BooleanFilterOperators = schemaComposer . createInputTC ( { name : 'boolean_filter_operators' , fields : { _eq : { type : GraphQLBoolean , } , _neq : { type : GraphQLBoolean , } , _null : { type : GraphQLBoolean , } , _nnull : { type : GraphQLBoolean , } , } , } ) ; const DateFilterOperators = schemaComposer . createInputTC ( { name : 'date_filter_operators' , fields : { _eq : { type : GraphQLString , } , _neq : { type : GraphQLString , } , _gt : { type : GraphQLString , } , _gte : { type : GraphQLString , } , _lt : { type : GraphQLString , } , _lte : { type : GraphQLString , } , _null : { type : GraphQLBoolean , } , _nnull : { type : GraphQLBoolean , } , } , } ) ; const NumberFilterOperators = schemaComposer . createInputTC ( { name : 'number_filter_operators' , fields : { _eq : { type : GraphQLFloat , } , _neq : { type : GraphQLFloat , } , _in : { type : new GraphQLList ( GraphQLFloat ) , } , _nin : { type : new GraphQLList ( GraphQLFloat ) , } , _gt : { type : GraphQLFloat , } , _gte : { type : GraphQLFloat , } , _lt : { type : GraphQLFloat , } , _lte : { type : GraphQLFloat , } , _null : { type : GraphQLBoolean , } , _nnull : { type : GraphQLBoolean , } , } , } ) ; const GeometryFilterOperators = schemaComposer . createInputTC ( { name : 'geometry_filter_operators' , fields : { _eq : { type : GraphQLGeoJSON , } , _neq : { type : GraphQLGeoJSON , } , _intersects : { type : GraphQLGeoJSON , } , _nintersects : { type : GraphQLGeoJSON , } , _intersects_bbox : { type : GraphQLGeoJSON , } , _nintersects_bbox : { type : GraphQLGeoJSON , } , } , } ) ; const DateFunctionFilterOperators = schemaComposer . createInputTC ( { name : 'date_function_filter_operators' , fields : { year : { type : NumberFilterOperators , } , month : { type : NumberFilterOperators , } , week : { type : NumberFilterOperators , } , day : { type : NumberFilterOperators , } , weekday : { type : NumberFilterOperators , } , } , } ) ; const TimeFunctionFilterOperators = schemaComposer . createInputTC ( { name : 'time_function_filter_operators' , fields : { hour : { type : NumberFilterOperators , } , minute : { type : NumberFilterOperators , } , second : { type : NumberFilterOperators , } , } , } ) ; const DateTimeFunctionFilterOperators = schemaComposer . createInputTC ( { name : 'datetime_function_filter_operators' , fields : { ... DateFunctionFilterOperators . getFields ( ) , ... TimeFunctionFilterOperators . getFields ( ) , } , } ) ; for ( const collection of Object . values ( schema . read . collections ) ) { if ( Object . keys ( collection . fields ) . length === 0 ) continue ; if ( SYSTEM_DENY_LIST . includes ( collection . collection ) ) continue ; ReadableCollectionFilterTypes [ collection . collection ] = schemaComposer . createInputTC ( { name : `${ collection . collection }_filter` , fields : Object . values ( collection . fields ) . reduce ( ( acc , field ) => { const graphqlType = getGraphQLType ( field . type ) ; let filterOperatorType ; switch ( graphqlType ) { case GraphQLBoolean : filterOperatorType = BooleanFilterOperators ; break ; case GraphQLInt : case GraphQLFloat : filterOperatorType = NumberFilterOperators ; break ; case GraphQLDate : filterOperatorType = DateFilterOperators ; break ; case GraphQLGeoJSON : filterOperatorType = GeometryFilterOperators ; break ; default : filterOperatorType = StringFilterOperators ; } acc [ field . field ] = filterOperatorType ; if ( field . type === 'date' ) { acc [ `${ field . field }_func` ] = { type : DateFunctionFilterOperators , } ; } if ( field . type === 'time' ) { acc [ `${ field . field }_func` ] = { type : TimeFunctionFilterOperators , } ; } if ( field . type === 'dateTime' || field . type === 'timestamp' ) { acc [ `${ field . field }_func` ] = { type : DateTimeFunctionFilterOperators , } ; } return acc ; } , { } as InputTypeComposerFieldConfigMapDefinition ) , } ) ; ReadableCollectionFilterTypes [ collection . collection ] . addFields ( { _and : [ ReadableCollectionFilterTypes [ collection . collection ] ] , _or : [ ReadableCollectionFilterTypes [ collection . collection ] ] , } ) ; AggregatedFilters [ collection . collection ] = schemaComposer . createObjectTC ( { name : `${ collection . collection }_aggregated_fields` , fields : Object . values ( collection . fields ) . reduce ( ( acc , field ) => { const graphqlType = getGraphQLType ( field . type ) ; switch ( graphqlType ) { case GraphQLInt : case GraphQLFloat : acc [ field . field ] = { type : GraphQLFloat , description : field . note , } ; break ; default : break ; } return acc ; } , { } as ObjectTypeComposerFieldConfigMapDefinition < any , any > ) , } ) ; AggregatedFunctions [ collection . collection ] = schemaComposer . createObjectTC ( { name : `${ collection . collection }_aggregated` , fields : { group : { name : 'group' , type : GraphQLJSON , } , avg : { name : 'avg' , type : AggregatedFilters [ collection . collection ] , } , sum : { name : 'sum' , type : AggregatedFilters [ collection . collection ] , } , count : { name : 'count' , type : AggregatedFilters [ collection . collection ] , } , countDistinct : { name : 'countDistinct' , type : AggregatedFilters [ collection . collection ] , } , avgDistinct : { name : 'avgDistinct' , type : AggregatedFilters [ collection . collection ] , } , sumDistinct : { name : 'sumDistinct' , type : AggregatedFilters [ collection . collection ] , } , min : { name : 'min' , type : AggregatedFilters [ collection . collection ] , } , max : { name : 'max' , type : AggregatedFilters [ collection . collection ] , } , } , } ) ; ReadCollectionTypes [ collection . collection ] . addResolver ( { name : collection . collection , args : collection . singleton ? undefined : { filter : ReadableCollectionFilterTypes [ collection . collection ] , sort : { type : new GraphQLList ( GraphQLString ) , } , limit : { type : GraphQLInt , } , offset : { type : GraphQLInt , } , page : { type : GraphQLInt , } , search : { type : GraphQLString , } , } , type : collection . singleton ? ReadCollectionTypes [ collection . collection ] : [ ReadCollectionTypes [ collection . collection ] ] , resolve : async ( { info , context } ) => { const result = await self . resolveQuery ( info ) ; context . data = result ; return result ; } , } ) ; ReadCollectionTypes [ collection . collection ] . addResolver ( { name : `${ collection . collection }_aggregated` , type : [ AggregatedFunctions [ collection . collection ] ] , args : { groupBy : new GraphQLList ( GraphQLString ) , filter : ReadableCollectionFilterTypes [ collection . collection ] , limit : { type : GraphQLInt , } , search : { type : GraphQLString , } , sort : { type : new GraphQLList ( GraphQLString ) , } , } , resolve : async ( { info , context } ) => { const result = await self . resolveQuery ( info ) ; context . data = result ; return result ; } , } ) ; if ( collection . singleton === false ) { ReadCollectionTypes [ collection . collection ] . addResolver ( { name : `${ collection . collection }_by_id` , type : ReadCollectionTypes [ collection . collection ] , args : { id : GraphQLNonNull ( GraphQLID ) , } , resolve : async ( { info , context } ) => { const result = await self . resolveQuery ( info ) ; context . data = result ; return result ; } , } ) ; } } for ( const relation of schema . read . relations ) { if ( relation . related_collection ) { if ( SYSTEM_DENY_LIST . includes ( relation . related_collection ) ) continue ; ReadableCollectionFilterTypes [ relation . collection ] ?. addFields ( { [ relation . field ] : ReadableCollectionFilterTypes [ relation . related_collection ] , } ) ; ReadCollectionTypes [ relation . collection ] ?. addFieldArgs ( relation . field , { filter : ReadableCollectionFilterTypes [ relation . related_collection ] , sort : { type : new GraphQLList ( GraphQLString ) , } , limit : { type : GraphQLInt , } , offset : { type : GraphQLInt , } , page : { type : GraphQLInt , } , search : { type : GraphQLString , } , } ) ; if ( relation . meta ?. one_field ) { ReadableCollectionFilterTypes [ relation . related_collection ] ?. addFields ( { [ relation . meta . one_field ] : ReadableCollectionFilterTypes [ relation . collection ] , } ) ; ReadCollectionTypes [ relation . related_collection ] ?. addFieldArgs ( relation . meta . one_field , { filter : ReadableCollectionFilterTypes [ relation . collection ] , sort : { type : new GraphQLList ( GraphQLString ) , } , limit : { type : GraphQLInt , } , offset : { type : GraphQLInt , } , page : { type : GraphQLInt , } , search : { type : GraphQLString , } , } ) ; } } else if ( relation . meta ?. one_allowed_collections ) { } } return { ReadCollectionTypes , ReadableCollectionFilterTypes } ; } function getWritableTypes ( ) { const { CollectionTypes : CreateCollectionTypes } = getTypes ( 'create' ) ; const { CollectionTypes : UpdateCollectionTypes } = getTypes ( 'update' ) ; const DeleteCollectionTypes = { } ; for ( const collection of Object . values ( schema . create . collections ) ) { if ( Object . keys ( collection . fields ) . length === 0 ) continue ; if ( SYSTEM_DENY_LIST . includes ( collection . collection ) ) continue ; if ( collection . collection in CreateCollectionTypes === false ) continue ; const collectionIsReadable = collection . collection in ReadCollectionTypes ; const creatableFields = CreateCollectionTypes [ collection . collection ] ?. getFields ( ) || { } ; if ( Object . keys ( creatableFields ) . length > 0 ) { CreateCollectionTypes [ collection . collection ] . addResolver ( { name : `create_${ collection . collection }_items` , type : collectionIsReadable ? [ ReadCollectionTypes [ collection . collection ] ] : GraphQLBoolean , args : collectionIsReadable ? ReadCollectionTypes [ collection . collection ] . getResolver ( collection . collection ) . getArgs ( ) : undefined , resolve : async ( { args , info } ) => await self . resolveMutation ( args , info ) , } ) ; CreateCollectionTypes [ collection . collection ] . addResolver ( { name : `create_${ collection . collection }_item` , type : collectionIsReadable ? ReadCollectionTypes [ collection . collection ] : GraphQLBoolean , resolve : async ( { args , info } ) => await self . resolveMutation ( args , info ) , } ) ; CreateCollectionTypes [ collection . collection ] . getResolver ( `create_${ collection . collection }_items` ) . addArgs ( { ... CreateCollectionTypes [ collection . collection ] . getResolver ( `create_${ collection . collection }_items` ) . getArgs ( ) , data : [ toInputObjectType ( CreateCollectionTypes [ collection . collection ] ) . setTypeName ( `create_${ collection . collection }_input` ) . NonNull , ] , } ) ; CreateCollectionTypes [ collection . collection ] . getResolver ( `create_${ collection . collection }_item` ) . addArgs ( { ... CreateCollectionTypes [ collection . collection ] . getResolver ( `create_${ collection . collection }_item` ) . getArgs ( ) , data : toInputObjectType ( CreateCollectionTypes [ collection . collection ] ) . setTypeName ( `create_${ collection . collection }_input` ) . NonNull , } ) ; } } for ( const collection of Object . values ( schema . update . collections ) ) { if ( Object . keys ( collection . fields ) . length === 0 ) continue ; if ( SYSTEM_DENY_LIST . includes ( collection . collection ) ) continue ; if ( collection . collection in UpdateCollectionTypes === false ) continue ; const collectionIsReadable = collection . collection in ReadCollectionTypes ; const updatableFields = UpdateCollectionTypes [ collection . collection ] ?. getFields ( ) || { } ; if ( Object . keys ( updatableFields ) . length > 0 ) { if ( collection . singleton ) { UpdateCollectionTypes [ collection . collection ] . addResolver ( { name : `update_${ collection . collection }` , type : collectionIsReadable ? ReadCollectionTypes [ collection . collection ] : GraphQLBoolean , args : { data : toInputObjectType ( UpdateCollectionTypes [ collection . collection ] ) . setTypeName ( `update_${ collection . collection }_input` ) . NonNull , } , resolve : async ( { args , info } ) => await self . resolveMutation ( args , info ) , } ) ; } else { UpdateCollectionTypes [ collection . collection ] . addResolver ( { name : `update_${ collection . collection }_items` , type : collectionIsReadable ? [ ReadCollectionTypes [ collection . collection ] ] : GraphQLBoolean , args : { ... ( collectionIsReadable ? ReadCollectionTypes [ collection . collection ] . getResolver ( collection . collection ) . getArgs ( ) : { } ) , ids : GraphQLNonNull ( new GraphQLList ( GraphQLID ) ) , data : toInputObjectType ( UpdateCollectionTypes [ collection . collection ] ) . setTypeName ( `update_${ collection . collection }_input` ) . NonNull , } , resolve : async ( { args , info } ) => await self . resolveMutation ( args , info ) , } ) ; UpdateCollectionTypes [ collection . collection ] . addResolver ( { name : `update_${ collection . collection }_item` , type : collectionIsReadable ? ReadCollectionTypes [ collection . collection ] : GraphQLBoolean , args : { id : GraphQLNonNull ( GraphQLID ) , data : toInputObjectType ( UpdateCollectionTypes [ collection . collection ] ) . setTypeName ( `update_${ collection . collection }_input` ) . NonNull , } , resolve : async ( { args , info } ) => await self . resolveMutation ( args , info ) , } ) ; } } } DeleteCollectionTypes . many = schemaComposer . createObjectTC ( { name : `delete_many` , fields : { ids : GraphQLNonNull ( new GraphQLList ( GraphQLID ) ) , } , } ) ; DeleteCollectionTypes . one = schemaComposer . createObjectTC ( { name : `delete_one` , fields : { id : GraphQLNonNull ( GraphQLID ) , } , } ) ; for ( const collection of Object . values ( schema . delete . collections ) ) { DeleteCollectionTypes . many . addResolver ( { name : `delete_${ collection . collection }_items` , type : DeleteCollectionTypes . many , args : { ids : GraphQLNonNull ( new GraphQLList ( GraphQLID ) ) , } , resolve : async ( { args , info } ) => await self . resolveMutation ( args , info ) , } ) ; DeleteCollectionTypes . one . addResolver ( { name : `delete_${ collection . collection }_item` , type : DeleteCollectionTypes . one , args : { id : GraphQLNonNull ( GraphQLID ) , } , resolve : async ( { args , info } ) => await self . resolveMutation ( args , info ) , } ) ; } return { CreateCollectionTypes , UpdateCollectionTypes , DeleteCollectionTypes } ; } } /**
     * Generic resolver that's used for every "regular" items/system query. Converts the incoming GraphQL AST / fragments into
     * Directus' query structure which is then executed by the services.
     */ async resolveQuery ( info ) { let collection = info . fieldName ; if ( this . scope === 'system' ) collection = `directus_${ collection }` ; const selections = this . replaceFragmentsInSelections ( info . fieldNodes [ 0 ] ?. selectionSet ?. selections , info . fragments ) ; if ( ! selections ) return null ; const args = this . parseArgs ( info . fieldNodes [ 0 ] . arguments || [ ] , info . variableValues ) ; let query ; const isAggregate = collection . endsWith ( '_aggregated' ) && collection in this . schema . collections === false ; if ( isAggregate ) { query = this . getAggregateQuery ( args , selections ) ; collection = collection . slice ( 0 , - 11 ) ; } else { query = this . getQuery ( args , selections , info . variableValues ) ; if ( collection . endsWith ( '_by_id' ) && collection in this . schema . collections === false ) { collection = collection . slice ( 0 , - 6 ) ; } } if ( args . id ) { query . filter = { _and : [ query . filter || { } , { [ this . schema . collections [ collection ] . primary ] : { _eq : args . id , } , } , ] , } ; query . limit = 1 ; } const result = await this . read ( collection , query ) ; if ( args . id ) { return result ?. [ 0 ] || null ; } if ( query . group ) { const aggregateKeys = Object . keys ( query . aggregate ?? { } ) ; result . map ( ( field ) => { field . group = omit ( field , aggregateKeys ) ; } ) ; } return result ; } async resolveMutation ( args , info ) { const action = info . fieldName . split ( '_' ) [ 0 ] as 'create' | 'update' | 'delete' ; let collection = info . fieldName . substring ( action . length + 1 ) ; if ( this . scope === 'system' ) collection = `directus_${ collection }` ; const selections = this . replaceFragmentsInSelections ( info . fieldNodes [ 0 ] ?. selectionSet ?. selections , info . fragments ) ; const query = this . getQuery ( args , selections || [ ] , info . variableValues ) ; const singleton = collection . endsWith ( '_items' ) === false && collection . endsWith ( '_item' ) === false && collection in this . schema . collections ; const single = collection . endsWith ( '_items' ) === false ; if ( collection . endsWith ( '_items' ) ) collection = collection . slice ( 0 , - 6 ) ; if ( collection . endsWith ( '_item' ) ) collection = collection . slice ( 0 , - 5 ) ; if ( singleton && action === 'update' ) { return await this . upsertSingleton ( collection , args . data , query ) ; } const service = this . getService ( collection ) ; const hasQuery = ( query . fields || [ ] ) . length > 0 ; try { if ( single ) { if ( action === 'create' ) { const key = await service . createOne ( args . data ) ; return hasQuery ? await service . readOne ( key , query ) : true ; } if ( action === 'update' ) { const key = await service . updateOne ( args . id , args . data ) ; return hasQuery ? await service . readOne ( key , query ) : true ; } if ( action === 'delete' ) { await service . deleteOne ( args . id ) ; return { id : args . id } ; } } else { if ( action === 'create' ) { const keys = await service . createMany ( args . data ) ; return hasQuery ? await service . readMany ( keys , query ) : true ; } if ( action === 'update' ) { const keys = await service . updateMany ( args . ids , args . data ) ; return hasQuery ? await service . readMany ( keys , query ) : true ; } if ( action === 'delete' ) { const keys = await service . deleteMany ( args . ids ) ; return { ids : keys } ; } } } catch ( err ) { return this . formatError ( err ) ; } } /**
     * Execute the read action on the correct service. Checks for singleton as well.
     */ async read ( collection , query ) { const service = this . getService ( collection ) ; const result = this . schema . collections [ collection ] . singleton ? await service . readSingleton ( query , { stripNonRequested : false } ) : await service . readByQuery ( query , { stripNonRequested : false } ) ; return result ; } /**
     * Upsert and read singleton item
     */ async upsertSingleton ( collection , body , query ) { const service = this . getService ( collection ) ; try { await service . upsertSingleton ( body ) ; if ( ( query . fields || [ ] ) . length > 0 ) { const result = await service . readSingleton ( query ) ; return result ; } return true ; } catch ( err ) { throw this . formatError ( err ) ; } } /**
     * GraphQL's regular resolver `args` variable only contains the "top-level" arguments. Seeing that we convert the
     * whole nested tree into one big query using Directus' own query resolver, we want to have a nested structure of
     * arguments for the whole resolving tree, which can later be transformed into Directus' AST using `deep`.
     * In order to do that, we'll parse over all ArgumentNodes and ObjectFieldNodes to manually recreate an object structure
     * of arguments
     */ parseArgs ( args , variableValues ) { if ( ! args || args . length === 0 ) return { } ; const parseObjectValue = ( arg ) => { return this . parseArgs ( arg . fields , variableValues ) ; } ; const argsObject = { } ; for ( const argument of args ) { if ( argument . value . kind === 'ObjectValue' ) { argsObject [ argument . name . value ] = parseObjectValue ( argument . value ) ; } else if ( argument . value . kind === 'Variable' ) { argsObject [ argument . name . value ] = variableValues [ argument . value . name . value ] ; } else if ( argument . value . kind === 'ListValue' ) { const values = [ ] ; for ( const valueNode of argument . value . values ) { if ( valueNode . kind === 'ObjectValue' ) { values . push ( this . parseArgs ( valueNode . fields , variableValues ) ) ; } else { values . push ( ( valueNode as any ) . value ) ; } } argsObject [ argument . name . value ] = values ; } else { argsObject [ argument . name . value ] = ( argument . value as IntValueNode | StringValueNode | BooleanValueNode ) . value ; } } return argsObject ; } /**
     * Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
     * Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
     */ getQuery ( rawQuery , selections , variableValues ) { const query = sanitizeQuery ( rawQuery , this . accountability ) ; const parseAliases = ( selections ) => { const aliases = { } ; for ( const selection of selections ) { if ( selection . kind !== 'Field' ) continue ; if ( selection . alias ?. value ) { aliases [ selection . alias . value ] = selection . name . value ; } } return aliases ; } ; const parseFields = ( selections , parent ? ) : string [ ] => { const fields = [ ] ; for ( let selection of selections ) { if ( ( selection . kind === 'Field' || selection . kind === 'InlineFragment' ) !== true ) continue ; selection = selection as FieldNode | InlineFragmentNode ; let current ; if ( selection . kind === 'InlineFragment' ) { if ( selection . typeCondition ! . name . value . startsWith ( '__' ) ) continue ; current = `${ parent }:${ selection . typeCondition ! . name . value }` ; } else { if ( selection . name . value . startsWith ( '__' ) ) continue ; current = selection . name . value ; if ( parent ) { current = `${ parent }.${ current }` ; } } if ( selection . selectionSet ) { let children ; if ( current . endsWith ( '_func' ) ) { children = [ ] ; const rootField = current . slice ( 0 , - 5 ) ; for ( const subSelection of selection . selectionSet . selections ) { if ( subSelection . kind !== 'Field' ) continue ; children . push ( `${ subSelection . name ! . value }(${ rootField })` ) ; } } else { children = parseFields ( selection . selectionSet . selections , current ) ; } fields . push ( ... children ) ; } else { fields . push ( current ) ; } if ( selection . kind === 'Field' && selection . arguments && selection . arguments . length > 0 ) { if ( selection . arguments && selection . arguments . length > 0 ) { if ( ! query . deep ) query . deep = { } ; const args = this . parseArgs ( selection . arguments , variableValues ) ; set ( query . deep , current , merge ( get ( query . deep , current ) , mapKeys ( sanitizeQuery ( args , this . accountability ) , ( value , key ) => `_${ key }` ) ) ) ; } } } return uniq ( fields ) ; } ; const replaceFuncs = ( filter ? ) : null | undefined | Filter => { if ( ! filter ) return filter ; return replaceFuncDeep ( filter ) ; function replaceFuncDeep ( filter ) { return transform ( filter , ( result , value , key ) => { let currentKey = key ; if ( typeof key === 'string' && key . endsWith ( '_func' ) ) { const functionName = Object . keys ( value ) [ 0 ] ! ; currentKey = `${ functionName }(${ currentKey . slice ( 0 , - 5 ) })` ; result [ currentKey ] = Object . values ( value ) [ 0 ] ! ; } else { result [ currentKey ] = isObject ( value ) ? replaceFuncDeep ( value ) : value ; } } ) ; } } ; query . alias = parseAliases ( selections ) ; query . fields = parseFields ( selections ) ; query . filter = replaceFuncs ( query . filter ) ; validateQuery ( query ) ; return query ; } /**
     * Resolve the aggregation query based on the requested aggregated fields
     */ getAggregateQuery ( rawQuery , selections ) { const query = sanitizeQuery ( rawQuery , this . accountability ) ; query . aggregate = { } ; for ( let aggregationGroup of selections ) { if ( ( aggregationGroup . kind === 'Field' ) !== true ) continue ; aggregationGroup = aggregationGroup as FieldNode ; if ( aggregationGroup . name . value . startsWith ( '__' ) ) continue ; const aggregateProperty = aggregationGroup . name . value as keyof Aggregate ; query . aggregate [ aggregateProperty ] = aggregationGroup . selectionSet ?. selections . filter ( ( selectionNode ) => ! ( selectionNode as FieldNode ) ?. name . value . startsWith ( '__' ) ) . map ( ( selectionNode ) => { selectionNode = selectionNode as FieldNode ; return selectionNode . name . value ; } ) ?? [ ] ; } validateQuery ( query ) ; return query ; } /**
     * Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
     */ formatError ( error ) { if ( Array . isArray ( error ) ) { return new GraphQLError ( error [ 0 ] . message , undefined , undefined , undefined , undefined , error [ 0 ] ) ; } return new GraphQLError ( error . message , undefined , undefined , undefined , undefined , error ) ; } /**
     * Select the correct service for the given collection. This allows the individual services to run
     * their custom checks (f.e. it allows UsersService to prevent updating TFA secret from outside)
     */ getService ( collection ) { const opts = { knex : this . knex , accountability : this . accountability , schema : this . schema , } ; switch ( collection ) { case 'directus_activity' : return new ActivityService ( opts ) ; case 'directus_files' : return new FilesService ( opts ) ; case 'directus_folders' : return new FoldersService ( opts ) ; case 'directus_permissions' : return new PermissionsService ( opts ) ; case 'directus_presets' : return new PresetsService ( opts ) ; case 'directus_notifications' : return new NotificationsService ( opts ) ; case 'directus_revisions' : return new RevisionsService ( opts ) ; case 'directus_roles' : return new RolesService ( opts ) ; case 'directus_settings' : return new SettingsService ( opts ) ; case 'directus_users' : return new UsersService ( opts ) ; case 'directus_webhooks' : return new WebhooksService ( opts ) ; case 'directus_shares' : return new SharesService ( opts ) ; default : return new ItemsService ( collection , opts ) ; } } /**
     * Replace all fragments in a selectionset for the actual selection set as defined in the fragment
     * Effectively merges the selections with the fragments used in those selections
     */ replaceFragmentsInSelections ( selections , fragments ) { if ( ! selections ) return null ; const result = flatten ( selections . map ( ( selection ) => { if ( selection . kind === 'FragmentSpread' ) { return this . replaceFragmentsInSelections ( fragments [ selection . name . value ] . selectionSet . selections , fragments ) ; } if ( ( selection . kind === 'Field' || selection . kind === 'InlineFragment' ) && selection . selectionSet ) { selection . selectionSet . selections = this . replaceFragmentsInSelections ( selection . selectionSet . selections , fragments ) as readonly SelectionNode [ ] ; } return selection ; } ) ) . filter ( ( s ) => s ) as SelectionNode [ ] ; return result ; } injectSystemResolvers ( schemaComposer , { CreateCollectionTypes , ReadCollectionTypes , UpdateCollectionTypes , DeleteCollectionTypes , } , schema ) { const AuthTokens = schemaComposer . createObjectTC ( { name : 'auth_tokens' , fields : { access_token : GraphQLString , expires : GraphQLInt , refresh_token : GraphQLString , } , } ) ; const AuthMode = new GraphQLEnumType ( { name : 'auth_mode' , values : { json : { value : 'json' } , cookie : { value : 'cookie' } , } , } ) ; const ServerInfo = schemaComposer . createObjectTC ( { name : 'server_info' , fields : { project_name : { type : GraphQLString } , project_logo : { type : GraphQLString } , project_color : { type : GraphQLString } , project_foreground : { type : GraphQLString } , project_background : { type : GraphQLString } , project_note : { type : GraphQLString } , custom_css : { type : GraphQLString } , } , } ) ; if ( this . accountability ?. admin === true ) { ServerInfo . addFields ( { directus : { type : new GraphQLObjectType ( { name : 'server_info_directus' , fields : { version : { type : GraphQLString , } , } , } ) , } , node : { type : new GraphQLObjectType ( { name : 'server_info_node' , fields : { version : { type : GraphQLString , } , uptime : { type : GraphQLInt , } , } , } ) , } , os : { type : new GraphQLObjectType ( { name : 'server_info_os' , fields : { type : { type : GraphQLString , } , version : { type : GraphQLString , } , uptime : { type : GraphQLInt , } , totalmem : { type : GraphQLInt , } , } , } ) , } , } ) ; } /** Globally available query */ schemaComposer . Query . addFields ( { extensions : { type : schemaComposer . createObjectTC ( { name : 'extensions' , fields : { interfaces : new GraphQLList ( GraphQLString ) , displays : new GraphQLList ( GraphQLString ) , layouts : new GraphQLList ( GraphQLString ) , modules : new GraphQLList ( GraphQLString ) , } , } ) , resolve : async ( ) => { const extensionManager = getExtensionManager ( ) ; return { interfaces : extensionManager . getExtensionsList ( 'interface' ) , displays : extensionManager . getExtensionsList ( 'display' ) , layouts : extensionManager . getExtensionsList ( 'layout' ) , modules : extensionManager . getExtensionsList ( 'module' ) , } ; } , } , server_specs_oas : { type : GraphQLJSON , resolve : async ( ) => { const service = new SpecificationService ( { schema : this . schema , accountability : this . accountability } ) ; return await service . oas . generate ( ) ; } , } , server_specs_graphql : { type : GraphQLString , args : { scope : new GraphQLEnumType ( { name : 'graphql_sdl_scope' , values : { items : { value : 'items' } , system : { value : 'system' } , } , } ) , } , resolve : async ( _ , args ) => { const service = new GraphQLService ( { schema : this . schema , accountability : this . accountability , scope : args . scope ?? 'items' , } ) ; return service . getSchema ( 'sdl' ) ; } , } , server_ping : { type : GraphQLString , resolve : ( ) => 'pong' , } , server_info : { type : ServerInfo , resolve : async ( ) => { const service = new ServerService ( { accountability : this . accountability , schema : this . schema , } ) ; return await service . serverInfo ( ) ; } , } , server_health : { type : GraphQLJSON , resolve : async ( ) => { const service = new ServerService ( { accountability : this . accountability , schema : this . schema , } ) ; return await service . serverInfo ( ) ; } , } , } ) ; const Collection = schemaComposer . createObjectTC ( { name : 'directus_collections' , } ) ; const Field = schemaComposer . createObjectTC ( { name : 'directus_fields' , } ) ; const Relation = schemaComposer . createObjectTC ( { name : 'directus_relations' , } ) ; /**
         * Globally available mutations
         */ schemaComposer . Mutation . addFields ( { auth_login : { type : AuthTokens , args : { email : GraphQLNonNull ( GraphQLString ) , password : GraphQLNonNull ( GraphQLString ) , mode : AuthMode , otp : GraphQLString , } , resolve : async ( _ , args , { req , res } ) => { const accountability = { ip : req ?. ip , userAgent : req ?. get ( 'user-agent' ) , role : null , } ; const authenticationService = new AuthenticationService ( { accountability : accountability , schema : this . schema , } ) ; const result = await authenticationService . login ( DEFAULT_AUTH_PROVIDER , args , args ?. otp ) ; if ( args . mode === 'cookie' ) { res ?. cookie ( env . REFRESH_TOKEN_COOKIE_NAME , result . refreshToken , { httpOnly : true , domain : env . REFRESH_TOKEN_COOKIE_DOMAIN , maxAge : ms ( env . REFRESH_TOKEN_TTL as string ) , secure : env . REFRESH_TOKEN_COOKIE_SECURE ?? false , sameSite : ( env . REFRESH_TOKEN_COOKIE_SAME_SITE as 'lax' | 'strict' | 'none' ) || 'strict' , } ) ; } return { access_token : result . accessToken , expires : result . expires , refresh_token : result . refreshToken , } ; } , } , auth_refresh : { type : AuthTokens , args : { refresh_token : GraphQLString , mode : AuthMode , } , resolve : async ( _ , args , { req , res } ) => { const accountability = { ip : req ?. ip , userAgent : req ?. get ( 'user-agent' ) , role : null , } ; const authenticationService = new AuthenticationService ( { accountability : accountability , schema : this . schema , } ) ; const currentRefreshToken = args . refresh_token || req ?. cookies [ env . REFRESH_TOKEN_COOKIE_NAME ] ; if ( ! currentRefreshToken ) { throw new InvalidPayloadException ( `"refresh_token" is required in either the JSON payload or Cookie` ) ; } const result = await authenticationService . refresh ( currentRefreshToken ) ; if ( args . mode === 'cookie' ) { res ?. cookie ( env . REFRESH_TOKEN_COOKIE_NAME , result . refreshToken , { httpOnly : true , domain : env . REFRESH_TOKEN_COOKIE_DOMAIN , maxAge : ms ( env . REFRESH_TOKEN_TTL as string ) , secure : env . REFRESH_TOKEN_COOKIE_SECURE ?? false , sameSite : ( env . REFRESH_TOKEN_COOKIE_SAME_SITE as 'lax' | 'strict' | 'none' ) || 'strict' , } ) ; } return { access_token : result . accessToken , expires : result . expires , refresh_token : result . refreshToken , } ; } , } , auth_logout : { type : GraphQLBoolean , args : { refresh_token : GraphQLString , } , resolve : async ( _ , args , { req } ) => { const accountability = { ip : req ?. ip , userAgent : req ?. get ( 'user-agent' ) , role : null , } ; const authenticationService = new AuthenticationService ( { accountability : accountability , schema : this . schema , } ) ; const currentRefreshToken = args . refresh_token || req ?. cookies [ env . REFRESH_TOKEN_COOKIE_NAME ] ; if ( ! currentRefreshToken ) { throw new InvalidPayloadException ( `"refresh_token" is required in either the JSON payload or Cookie` ) ; } await authenticationService . logout ( currentRefreshToken ) ; return true ; } , } , auth_password_request : { type : GraphQLBoolean , args : { email : GraphQLNonNull ( GraphQLString ) , reset_url : GraphQLString , } , resolve : async ( _ , args , { req } ) => { const accountability = { ip : req ?. ip , userAgent : req ?. get ( 'user-agent' ) , role : null , } ; const service = new UsersService ( { accountability , schema : this . schema } ) ; try { await service . requestPasswordReset ( args . email , args . reset_url || null ) ; } catch ( err ) { if ( err instanceof InvalidPayloadException ) { throw err ; } } return true ; } , } , auth_password_reset : { type : GraphQLBoolean , args : { token : GraphQLNonNull ( GraphQLString ) , password : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args , { req } ) => { const accountability = { ip : req ?. ip , userAgent : req ?. get ( 'user-agent' ) , role : null , } ; const service = new UsersService ( { accountability , schema : this . schema } ) ; await service . resetPassword ( args . token , args . password ) ; return true ; } , } , users_me_tfa_generate : { type : new GraphQLObjectType ( { name : 'users_me_tfa_generate_data' , fields : { secret : { type : GraphQLString } , otpauth_url : { type : GraphQLString } , } , } ) , args : { password : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { if ( ! this . accountability ?. user ) return null ; const service = new TFAService ( { accountability : this . accountability , schema : this . schema , } ) ; const authService = new AuthenticationService ( { accountability : this . accountability , schema : this . schema , } ) ; await authService . verifyPassword ( this . accountability . user , args . password ) ; const { url , secret } = await service . generateTFA ( this . accountability . user ) ; return { secret , otpauth_url : url } ; } , } , users_me_tfa_enable : { type : GraphQLBoolean , args : { otp : GraphQLNonNull ( GraphQLString ) , secret : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { if ( ! this . accountability ?. user ) return null ; const service = new TFAService ( { accountability : this . accountability , schema : this . schema , } ) ; await service . enableTFA ( this . accountability . user , args . otp , args . secret ) ; return true ; } , } , users_me_tfa_disable : { type : GraphQLBoolean , args : { otp : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { if ( ! this . accountability ?. user ) return null ; const service = new TFAService ( { accountability : this . accountability , schema : this . schema , } ) ; const otpValid = await service . verifyOTP ( this . accountability . user , args . otp ) ; if ( otpValid === false ) { throw new InvalidPayloadException ( `"otp" is invalid` ) ; } await service . disableTFA ( this . accountability . user ) ; return true ; } , } , utils_hash_generate : { type : GraphQLString , args : { string : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { return await generateHash ( args . string ) ; } , } , utils_hash_verify : { type : GraphQLBoolean , args : { string : GraphQLNonNull ( GraphQLString ) , hash : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { return await argon2 . verify ( args . hash , args . string ) ; } , } , utils_sort : { type : GraphQLBoolean , args : { collection : GraphQLNonNull ( GraphQLString ) , item : GraphQLNonNull ( GraphQLID ) , to : GraphQLNonNull ( GraphQLID ) , } , resolve : async ( _ , args ) => { const service = new UtilsService ( { accountability : this . accountability , schema : this . schema , } ) ; const { item , to } = args ; await service . sort ( args . collection , { item , to } ) ; return true ; } , } , utils_revert : { type : GraphQLBoolean , args : { revision : GraphQLNonNull ( GraphQLID ) , } , resolve : async ( _ , args ) => { const service = new RevisionsService ( { accountability : this . accountability , schema : this . schema , } ) ; await service . revert ( args . revision ) ; return true ; } , } , utils_cache_clear : { type : GraphQLVoid , resolve : async ( ) => { if ( this . accountability ?. admin !== true ) { throw new ForbiddenException ( ) ; } const { cache , systemCache } = getCache ( ) ; await cache ?. clear ( ) ; await systemCache . clear ( ) ; return ; } , } , users_invite_accept : { type : GraphQLBoolean , args : { token : GraphQLNonNull ( GraphQLString ) , password : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { const service = new UsersService ( { accountability : this . accountability , schema : this . schema , } ) ; await service . acceptInvite ( args . token , args . password ) ; return true ; } , } , } ) ; if ( 'directus_collections' in schema . read . collections ) { Collection . addFields ( { collection : GraphQLString , meta : schemaComposer . createObjectTC ( { name : 'directus_collections_meta' , fields : Object . values ( schema . read . collections [ 'directus_collections' ] . fields ) . reduce ( ( acc , field ) => { acc [ field . field ] = { type : field . nullable ? getGraphQLType ( field . type ) : GraphQLNonNull ( getGraphQLType ( field . type ) ) , description : field . note , } ; return acc ; } , { } as ObjectTypeComposerFieldConfigMapDefinition < any , any > ) , } ) , schema : schemaComposer . createObjectTC ( { name : 'directus_collections_schema' , fields : { name : GraphQLString , comment : GraphQLString , } , } ) , } ) ; schemaComposer . Query . addFields ( { collections : { type : [ Collection ] , resolve : async ( ) => { const collectionsService = new CollectionsService ( { accountability : this . accountability , schema : this . schema , } ) ; return await collectionsService . readByQuery ( ) ; } , } , collections_by_name : { type : Collection , args : { name : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { const collectionsService = new CollectionsService ( { accountability : this . accountability , schema : this . schema , } ) ; return await collectionsService . readOne ( args . name ) ; } , } , } ) ; } if ( 'directus_fields' in schema . read . collections ) { Field . addFields ( { collection : GraphQLString , field : GraphQLString , type : GraphQLString , meta : schemaComposer . createObjectTC ( { name : 'directus_fields_meta' , fields : Object . values ( schema . read . collections [ 'directus_fields' ] . fields ) . reduce ( ( acc , field ) => { acc [ field . field ] = { type : field . nullable ? getGraphQLType ( field . type ) : GraphQLNonNull ( getGraphQLType ( field . type ) ) , description : field . note , } ; return acc ; } , { } as ObjectTypeComposerFieldConfigMapDefinition < any , any > ) , } ) , schema : schemaComposer . createObjectTC ( { name : 'directus_fields_schema' , fields : { name : GraphQLString , table : GraphQLString , data_type : GraphQLString , default_value : GraphQLString , max_length : GraphQLInt , numeric_precision : GraphQLInt , numeric_scale : GraphQLInt , is_nullable : GraphQLBoolean , is_unique : GraphQLBoolean , is_primary_key : GraphQLBoolean , has_auto_increment : GraphQLBoolean , foreign_key_column : GraphQLString , foreign_key_table : GraphQLString , comment : GraphQLString , } , } ) , } ) ; schemaComposer . Query . addFields ( { fields : { type : [ Field ] , resolve : async ( ) => { const service = new FieldsService ( { accountability : this . accountability , schema : this . schema , } ) ; return await service . readAll ( ) ; } , } , fields_in_collection : { type : [ Field ] , args : { collection : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { const service = new FieldsService ( { accountability : this . accountability , schema : this . schema , } ) ; return await service . readAll ( args . collection ) ; } , } , fields_by_name : { type : Field , args : { collection : GraphQLNonNull ( GraphQLString ) , field : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { const service = new FieldsService ( { accountability : this . accountability , schema : this . schema , } ) ; return await service . readOne ( args . collection , args . field ) ; } , } , } ) ; } if ( 'directus_relations' in schema . read . collections ) { Relation . addFields ( { collection : GraphQLString , field : GraphQLString , related_collection : GraphQLString , schema : schemaComposer . createObjectTC ( { name : 'directus_relations_schema' , fields : { table : GraphQLNonNull ( GraphQLString ) , column : GraphQLNonNull ( GraphQLString ) , foreign_key_table : GraphQLNonNull ( GraphQLString ) , foreign_key_column : GraphQLNonNull ( GraphQLString ) , constraint_name : GraphQLString , on_update : GraphQLNonNull ( GraphQLString ) , on_delete : GraphQLNonNull ( GraphQLString ) , } , } ) , meta : schemaComposer . createObjectTC ( { name : 'directus_relations_meta' , fields : Object . values ( schema . read . collections [ 'directus_relations' ] . fields ) . reduce ( ( acc , field ) => { acc [ field . field ] = { type : getGraphQLType ( field . type ) , description : field . note , } ; return acc ; } , { } as ObjectTypeComposerFieldConfigMapDefinition < any , any > ) , } ) , } ) ; schemaComposer . Query . addFields ( { relations : { type : [ Relation ] , resolve : async ( ) => { const service = new RelationsService ( { accountability : this . accountability , schema : this . schema , } ) ; return await service . readAll ( ) ; } , } , relations_in_collection : { type : [ Relation ] , args : { collection : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { const service = new RelationsService ( { accountability : this . accountability , schema : this . schema , } ) ; return await service . readAll ( args . collection ) ; } , } , relations_by_name : { type : Relation , args : { collection : GraphQLNonNull ( GraphQLString ) , field : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { const service = new RelationsService ( { accountability : this . accountability , schema : this . schema , } ) ; return await service . readOne ( args . collection , args . field ) ; } , } , } ) ; } if ( this . accountability ?. admin === true ) { schemaComposer . Mutation . addFields ( { create_collections_item : { type : Collection , args : { data : toInputObjectType ( Collection . clone ( 'create_directus_collections' ) , { postfix : '_input' , } ) . addFields ( { fields : [ toInputObjectType ( Field . clone ( 'create_directus_collections_fields' ) , { postfix : '_input' } ) . NonNull , ] , } ) . NonNull , } , resolve : async ( _ , args ) => { const collectionsService = new CollectionsService ( { accountability : this . accountability , schema : this . schema , } ) ; const collectionKey = await collectionsService . createOne ( args . data ) ; return await collectionsService . readOne ( collectionKey ) ; } , } , update_collections_item : { type : Collection , args : { collection : GraphQLNonNull ( GraphQLString ) , data : toInputObjectType ( Collection . clone ( 'update_directus_collections' ) , { postfix : '_input' , } ) . removeField ( [ 'collection' , 'schema' ] ) . NonNull , } , resolve : async ( _ , args ) => { const collectionsService = new CollectionsService ( { accountability : this . accountability , schema : this . schema , } ) ; const collectionKey = await collectionsService . updateOne ( args . collection , args . data ) ; return await collectionsService . readOne ( collectionKey ) ; } , } , delete_collections_item : { type : schemaComposer . createObjectTC ( { name : 'delete_collection' , fields : { collection : GraphQLString , } , } ) , args : { collection : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { const collectionsService = new CollectionsService ( { accountability : this . accountability , schema : this . schema , } ) ; await collectionsService . deleteOne ( args . collection ) ; return { collection : args . collection } ; } , } , } ) ; schemaComposer . Mutation . addFields ( { create_fields_item : { type : Field , args : { collection : GraphQLNonNull ( GraphQLString ) , data : toInputObjectType ( Field . clone ( 'create_directus_fields' ) , { postfix : '_input' } ) . NonNull , } , resolve : async ( _ , args ) => { const service = new FieldsService ( { accountability : this . accountability , schema : this . schema , } ) ; await service . createField ( args . collection , args . data ) ; return await service . readOne ( args . collection , args . data . field ) ; } , } , update_fields_item : { type : Field , args : { collection : GraphQLNonNull ( GraphQLString ) , field : GraphQLNonNull ( GraphQLString ) , data : toInputObjectType ( Field . clone ( 'update_directus_fields' ) , { postfix : '_input' } ) . NonNull , } , resolve : async ( _ , args ) => { const service = new FieldsService ( { accountability : this . accountability , schema : this . schema , } ) ; await service . updateField ( args . collection , { ... args . data , field : args . field , } ) ; return await service . readOne ( args . collection , args . data . field ) ; } , } , delete_fields_item : { type : schemaComposer . createObjectTC ( { name : 'delete_field' , fields : { collection : GraphQLString , field : GraphQLString , } , } ) , args : { collection : GraphQLNonNull ( GraphQLString ) , field : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { const service = new FieldsService ( { accountability : this . accountability , schema : this . schema , } ) ; await service . deleteField ( args . collection , args . field ) ; const { collection , field } = args ; return { collection , field } ; } , } , } ) ; schemaComposer . Mutation . addFields ( { create_relations_item : { type : Relation , args : { data : toInputObjectType ( Relation . clone ( 'create_directus_relations' ) , { postfix : '_input' } ) . NonNull , } , resolve : async ( _ , args ) => { const relationsService = new RelationsService ( { accountability : this . accountability , schema : this . schema , } ) ; await relationsService . createOne ( args . data ) ; return await relationsService . readOne ( args . data . collection , args . data . field ) ; } , } , update_relations_item : { type : Relation , args : { collection : GraphQLNonNull ( GraphQLString ) , field : GraphQLNonNull ( GraphQLString ) , data : toInputObjectType ( Relation . clone ( 'update_directus_relations' ) , { postfix : '_input' } ) . NonNull , } , resolve : async ( _ , args ) => { const relationsService = new RelationsService ( { accountability : this . accountability , schema : this . schema , } ) ; await relationsService . updateOne ( args . collection , args . field , args . data ) ; return await relationsService . readOne ( args . data . collection , args . data . field ) ; } , } , delete_relations_item : { type : schemaComposer . createObjectTC ( { name : 'delete_relation' , fields : { collection : GraphQLString , field : GraphQLString , } , } ) , args : { collection : GraphQLNonNull ( GraphQLString ) , field : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args ) => { const relationsService = new RelationsService ( { accountability : this . accountability , schema : this . schema , } ) ; await relationsService . deleteOne ( args . collection , args . field ) ; return { collection : args . collection , field : args . field } ; } , } , } ) ; } if ( 'directus_users' in schema . read . collections ) { schemaComposer . Query . addFields ( { users_me : { type : ReadCollectionTypes [ 'directus_users' ] , resolve : async ( _ , args , __ , info ) => { if ( ! this . accountability ?. user ) return null ; const service = new UsersService ( { schema : this . schema , accountability : this . accountability } ) ; const selections = this . replaceFragmentsInSelections ( info . fieldNodes [ 0 ] ?. selectionSet ?. selections , info . fragments ) ; const query = this . getQuery ( args , selections || [ ] , info . variableValues ) ; return await service . readOne ( this . accountability . user , query ) ; } , } , } ) ; } if ( 'directus_users' in schema . update . collections ) { schemaComposer . Mutation . addFields ( { update_users_me : { type : ReadCollectionTypes [ 'directus_users' ] , args : { data : toInputObjectType ( UpdateCollectionTypes [ 'directus_users' ] ) , } , resolve : async ( _ , args , __ , info ) => { if ( ! this . accountability ?. user ) return null ; const service = new UsersService ( { schema : this . schema , accountability : this . accountability , } ) ; await service . updateOne ( this . accountability . user , args . data ) ; if ( 'directus_users' in ReadCollectionTypes ) { const selections = this . replaceFragmentsInSelections ( info . fieldNodes [ 0 ] ?. selectionSet ?. selections , info . fragments ) ; const query = this . getQuery ( args , selections || [ ] , info . variableValues ) ; return await service . readOne ( this . accountability . user , query ) ; } return true ; } , } , } ) ; } if ( 'directus_activity' in schema . create . collections ) { schemaComposer . Mutation . addFields ( { create_comment : { type : ReadCollectionTypes [ 'directus_activity' ] ?? GraphQLBoolean , args : { collection : GraphQLNonNull ( GraphQLString ) , item : GraphQLNonNull ( GraphQLID ) , comment : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args , __ , info ) => { const service = new ActivityService ( { accountability : this . accountability , schema : this . schema , } ) ; const primaryKey = await service . createOne ( { ... args , action : Action . COMMENT , user : this . accountability ?. user , ip : this . accountability ?. ip , user_agent : this . accountability ?. userAgent , } ) ; if ( 'directus_activity' in ReadCollectionTypes ) { const selections = this . replaceFragmentsInSelections ( info . fieldNodes [ 0 ] ?. selectionSet ?. selections , info . fragments ) ; const query = this . getQuery ( args , selections || [ ] , info . variableValues ) ; return await service . readOne ( primaryKey , query ) ; } return true ; } , } , } ) ; } if ( 'directus_activity' in schema . update . collections ) { schemaComposer . Mutation . addFields ( { update_comment : { type : ReadCollectionTypes [ 'directus_activity' ] ?? GraphQLBoolean , args : { id : GraphQLNonNull ( GraphQLID ) , comment : GraphQLNonNull ( GraphQLString ) , } , resolve : async ( _ , args , __ , info ) => { const service = new ActivityService ( { accountability : this . accountability , schema : this . schema , } ) ; const primaryKey = await service . updateOne ( args . id , { comment : args . comment } ) ; if ( 'directus_activity' in ReadCollectionTypes ) { const selections = this . replaceFragmentsInSelections ( info . fieldNodes [ 0 ] ?. selectionSet ?. selections , info . fragments ) ; const query = this . getQuery ( args , selections || [ ] , info . variableValues ) ; return await service . readOne ( primaryKey , query ) ; } return true ; } , } , } ) ; } if ( 'directus_activity' in schema . delete . collections ) { schemaComposer . Mutation . addFields ( { delete_comment : { type : DeleteCollectionTypes . one , args : { id : GraphQLNonNull ( GraphQLID ) , } , resolve : async ( _ , args ) => { const service = new ActivityService ( { accountability : this . accountability , schema : this . schema , } ) ; await service . deleteOne ( args . id ) ; return { id : args . id } ; } , } , } ) ; } if ( 'directus_files' in schema . create . collections ) { schemaComposer . Mutation . addFields ( { import_file : { type : ReadCollectionTypes [ 'directus_files' ] ?? GraphQLBoolean , args : { url : GraphQLNonNull ( GraphQLString ) , data : toInputObjectType ( CreateCollectionTypes [ 'directus_files' ] ) . setTypeName ( 'create_directus_files_input' ) , } , resolve : async ( _ , args , __ , info ) => { const service = new FilesService ( { accountability : this . accountability , schema : this . schema , } ) ; const primaryKey = await service . importOne ( args . url , args . data ) ; if ( 'directus_files' in ReadCollectionTypes ) { const selections = this . replaceFragmentsInSelections ( info . fieldNodes [ 0 ] ?. selectionSet ?. selections , info . fragments ) ; const query = this . getQuery ( args , selections || [ ] , info . variableValues ) ; return await service . readOne ( primaryKey , query ) ; } return true ; } , } , } ) ; } if ( 'directus_users' in schema . create . collections ) { schemaComposer . Mutation . addFields ( { users_invite : { type : GraphQLBoolean , args : { email : GraphQLNonNull ( GraphQLString ) , role : GraphQLNonNull ( GraphQLString ) , invite_url : GraphQLString , } , resolve : async ( _ , args ) => { const service = new UsersService ( { accountability : this . accountability , schema : this . schema , } ) ; await service . inviteUser ( args . email , args . role , args . invite_url || null ) ; return true ; } , } , } ) ; } return schemaComposer ; } }