@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
454 lines (453 loc) • 21 kB
JavaScript
import { useEnv } from '@directus/env';
import { toBoolean } from '@directus/utils';
import { GraphQLBoolean, GraphQLEnumType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString, } from 'graphql';
import { GraphQLJSON, SchemaComposer, toInputObjectType } from 'graphql-compose';
import getDatabase from '../../../database/index.js';
import { fetchAccountabilityCollectionAccess } from '../../../permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js';
import { fetchAccountabilityPolicyGlobals } from '../../../permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js';
import { CollectionsService } from '../../collections.js';
import { FieldsService } from '../../fields.js';
import { FilesService } from '../../files.js';
import { RelationsService } from '../../relations.js';
import { RolesService } from '../../roles.js';
import { ServerService } from '../../server.js';
import { SpecificationService } from '../../specifications.js';
import { UsersService } from '../../users.js';
import { GraphQLService } from '../index.js';
import { generateSchema } from '../schema/index.js';
import { getQuery } from '../schema/parse-query.js';
import { replaceFragmentsInSelections } from '../utils/replace-fragments.js';
import { getCollectionType } from './get-collection-type.js';
import { getFieldType } from './get-field-type.js';
import { getRelationType } from './get-relation-type.js';
import { resolveSystemAdmin } from './system-admin.js';
import { globalResolvers } from './system-global.js';
const env = useEnv();
export function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTypes, ReadCollectionTypes, UpdateCollectionTypes }, schema) {
globalResolvers(gql, schemaComposer);
const ServerInfo = schemaComposer.createObjectTC({
name: 'server_info',
fields: {
project: {
type: new GraphQLObjectType({
name: 'server_info_project',
fields: {
project_name: { type: GraphQLString },
project_descriptor: { type: GraphQLString },
project_logo: { type: GraphQLString },
project_color: { type: GraphQLString },
default_language: { type: GraphQLString },
public_foreground: { type: GraphQLString },
public_background: { type: GraphQLString },
public_note: { type: GraphQLString },
custom_css: { type: GraphQLString },
public_registration: { type: GraphQLBoolean },
public_registration_verify_email: { type: GraphQLBoolean },
},
}),
},
},
});
if (gql.accountability?.user) {
ServerInfo.addFields({
rateLimit: env['RATE_LIMITER_ENABLED']
? {
type: new GraphQLObjectType({
name: 'server_info_rate_limit',
fields: {
points: { type: GraphQLInt },
duration: { type: GraphQLInt },
},
}),
}
: GraphQLBoolean,
rateLimitGlobal: env['RATE_LIMITER_GLOBAL_ENABLED']
? {
type: new GraphQLObjectType({
name: 'server_info_rate_limit_global',
fields: {
points: { type: GraphQLInt },
duration: { type: GraphQLInt },
},
}),
}
: GraphQLBoolean,
websocket: toBoolean(env['WEBSOCKETS_ENABLED'])
? {
type: new GraphQLObjectType({
name: 'server_info_websocket',
fields: {
rest: {
type: toBoolean(env['WEBSOCKETS_REST_ENABLED'])
? new GraphQLObjectType({
name: 'server_info_websocket_rest',
fields: {
authentication: {
type: new GraphQLEnumType({
name: 'server_info_websocket_rest_authentication',
values: {
public: { value: 'public' },
handshake: { value: 'handshake' },
strict: { value: 'strict' },
},
}),
},
path: { type: GraphQLString },
},
})
: GraphQLBoolean,
},
graphql: {
type: toBoolean(env['WEBSOCKETS_GRAPHQL_ENABLED'])
? new GraphQLObjectType({
name: 'server_info_websocket_graphql',
fields: {
authentication: {
type: new GraphQLEnumType({
name: 'server_info_websocket_graphql_authentication',
values: {
public: { value: 'public' },
handshake: { value: 'handshake' },
strict: { value: 'strict' },
},
}),
},
path: { type: GraphQLString },
},
})
: GraphQLBoolean,
},
heartbeat: {
type: toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED']) ? GraphQLInt : GraphQLBoolean,
},
},
}),
}
: GraphQLBoolean,
queryLimit: {
type: new GraphQLObjectType({
name: 'server_info_query_limit',
fields: {
default: { type: GraphQLInt },
max: { type: GraphQLInt },
},
}),
},
});
}
/** Globally available query */
schemaComposer.Query.addFields({
server_specs_oas: {
type: GraphQLJSON,
resolve: async () => {
const service = new SpecificationService({ schema: gql.schema, accountability: gql.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: gql.schema,
accountability: gql.accountability,
scope: args['scope'] ?? 'items',
});
return await generateSchema(service, 'sdl');
},
},
server_ping: {
type: GraphQLString,
resolve: () => 'pong',
},
server_info: {
type: ServerInfo,
resolve: async () => {
const service = new ServerService({
accountability: gql.accountability,
schema: gql.schema,
});
return await service.serverInfo();
},
},
server_health: {
type: GraphQLJSON,
resolve: async () => {
const service = new ServerService({
accountability: gql.accountability,
schema: gql.schema,
});
return await service.health();
},
},
});
if ('directus_collections' in schema.read.collections) {
const Collection = getCollectionType(schemaComposer, schema, 'read');
schemaComposer.Query.addFields({
collections: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Collection.getType()))),
resolve: async () => {
const collectionsService = new CollectionsService({
accountability: gql.accountability,
schema: gql.schema,
});
return await collectionsService.readByQuery();
},
},
collections_by_name: {
type: Collection,
args: {
name: new GraphQLNonNull(GraphQLString),
},
resolve: async (_, args) => {
const collectionsService = new CollectionsService({
accountability: gql.accountability,
schema: gql.schema,
});
return await collectionsService.readOne(args['name']);
},
},
});
}
if ('directus_fields' in schema.read.collections) {
const Field = getFieldType(schemaComposer, schema, 'read');
schemaComposer.Query.addFields({
fields: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Field.getType()))),
resolve: async () => {
const service = new FieldsService({
accountability: gql.accountability,
schema: gql.schema,
});
return await service.readAll();
},
},
fields_in_collection: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Field.getType()))),
args: {
collection: new GraphQLNonNull(GraphQLString),
},
resolve: async (_, args) => {
const service = new FieldsService({
accountability: gql.accountability,
schema: gql.schema,
});
return await service.readAll(args['collection']);
},
},
fields_by_name: {
type: Field,
args: {
collection: new GraphQLNonNull(GraphQLString),
field: new GraphQLNonNull(GraphQLString),
},
resolve: async (_, args) => {
const service = new FieldsService({
accountability: gql.accountability,
schema: gql.schema,
});
return await service.readOne(args['collection'], args['field']);
},
},
});
}
if ('directus_relations' in schema.read.collections) {
const Relation = getRelationType(schemaComposer, schema, 'read');
schemaComposer.Query.addFields({
relations: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Relation.getType()))),
resolve: async () => {
const service = new RelationsService({
accountability: gql.accountability,
schema: gql.schema,
});
return await service.readAll();
},
},
relations_in_collection: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Relation.getType()))),
args: {
collection: new GraphQLNonNull(GraphQLString),
},
resolve: async (_, args) => {
const service = new RelationsService({
accountability: gql.accountability,
schema: gql.schema,
});
return await service.readAll(args['collection']);
},
},
relations_by_name: {
type: Relation,
args: {
collection: new GraphQLNonNull(GraphQLString),
field: new GraphQLNonNull(GraphQLString),
},
resolve: async (_, args) => {
const service = new RelationsService({
accountability: gql.accountability,
schema: gql.schema,
});
return await service.readOne(args['collection'], args['field']);
},
},
});
}
resolveSystemAdmin(gql, schema, schemaComposer);
if ('directus_users' in schema.read.collections) {
schemaComposer.Query.addFields({
users_me: {
type: ReadCollectionTypes['directus_users'],
resolve: async (_, args, __, info) => {
if (!gql.accountability?.user)
return null;
const service = new UsersService({ schema: gql.schema, accountability: gql.accountability });
const selections = replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
const query = await getQuery(args, gql.schema, selections || [], info.variableValues, gql.accountability, 'directus_users');
return await service.readOne(gql.accountability.user, query);
},
},
});
}
if ('directus_permissions' in schema.read.collections) {
schemaComposer.Query.addFields({
permissions_me: {
type: schemaComposer.createScalarTC({
name: 'permissions_me_type',
parseValue: (value) => value,
serialize: (value) => value,
}),
resolve: async (_, _args, __, _info) => {
if (!gql.accountability?.user && !gql.accountability?.role)
return null;
const result = await fetchAccountabilityCollectionAccess(gql.accountability, {
schema: gql.schema,
knex: getDatabase(),
});
return result;
},
},
});
}
if ('directus_roles' in schema.read.collections) {
schemaComposer.Query.addFields({
roles_me: {
type: ReadCollectionTypes['directus_roles'].List,
resolve: async (_, args, __, info) => {
if (!gql.accountability?.user && !gql.accountability?.role)
return null;
const service = new RolesService({
accountability: gql.accountability,
schema: gql.schema,
});
const selections = replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
const query = await getQuery(args, gql.schema, selections || [], info.variableValues, gql.accountability, 'directus_roles');
query.limit = -1;
const roles = await service.readMany(gql.accountability.roles, query);
return roles;
},
},
});
}
if ('directus_policies' in schema.read.collections) {
schemaComposer.Query.addFields({
policies_me_globals: {
type: schemaComposer.createObjectTC({
name: 'policy_me_globals_type',
fields: {
enforce_tfa: 'Boolean',
app_access: 'Boolean',
admin_access: 'Boolean',
},
}),
resolve: async (_, _args, __, _info) => {
if (!gql.accountability?.user && !gql.accountability?.role)
return null;
const result = await fetchAccountabilityPolicyGlobals(gql.accountability, {
schema: gql.schema,
knex: getDatabase(),
});
return result;
},
},
});
}
if ('directus_users' in schema.update.collections && gql.accountability?.user) {
schemaComposer.Mutation.addFields({
update_users_me: {
type: ReadCollectionTypes['directus_users'],
args: {
data: toInputObjectType(UpdateCollectionTypes['directus_users']),
},
resolve: async (_, args, __, info) => {
if (!gql.accountability?.user)
return null;
const service = new UsersService({
schema: gql.schema,
accountability: gql.accountability,
});
await service.updateOne(gql.accountability.user, args['data']);
if ('directus_users' in ReadCollectionTypes) {
const selections = replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
const query = await getQuery(args, gql.schema, selections || [], info.variableValues, gql.accountability, 'directus_users');
return await service.readOne(gql.accountability.user, query);
}
return true;
},
},
});
}
if ('directus_files' in schema.create.collections) {
schemaComposer.Mutation.addFields({
import_file: {
type: ReadCollectionTypes['directus_files'] ?? GraphQLBoolean,
args: {
url: new GraphQLNonNull(GraphQLString),
data: toInputObjectType(CreateCollectionTypes['directus_files']).setTypeName('create_directus_files_input'),
},
resolve: async (_, args, __, info) => {
const service = new FilesService({
accountability: gql.accountability,
schema: gql.schema,
});
const primaryKey = await service.importOne(args['url'], args['data']);
if ('directus_files' in ReadCollectionTypes) {
const selections = replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
const query = await getQuery(args, gql.schema, selections || [], info.variableValues, gql.accountability, 'directus_files');
return await service.readOne(primaryKey, query);
}
return true;
},
},
});
}
if ('directus_users' in schema.create.collections) {
schemaComposer.Mutation.addFields({
users_invite: {
type: GraphQLBoolean,
args: {
email: new GraphQLNonNull(GraphQLString),
role: new GraphQLNonNull(GraphQLString),
invite_url: GraphQLString,
},
resolve: async (_, args) => {
const service = new UsersService({
accountability: gql.accountability,
schema: gql.schema,
});
await service.inviteUser(args['email'], args['role'], args['invite_url'] || null);
return true;
},
},
});
}
return schemaComposer;
}