@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
191 lines (190 loc) • 10.2 kB
JavaScript
import { useEnv } from '@directus/env';
import { isSystemCollection } from '@directus/system-data';
import { Semaphore } from 'async-mutex';
import { GraphQLSchema } from 'graphql';
import { SchemaComposer } from 'graphql-compose';
import { fetchAllowedFieldMap, } from '../../../permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js';
import { fetchInconsistentFieldMap } from '../../../permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js';
import { reduceSchema } from '../../../utils/reduce-schema.js';
import { GraphQLService } from '../index.js';
import { injectSystemResolvers } from '../resolvers/system.js';
import { cache } from '../schema-cache.js';
import { GraphQLVoid } from '../types/void.js';
import { sanitizeGraphqlSchema } from '../utils/sanitize-gql-schema.js';
import { getReadableTypes } from './read.js';
import { getWritableTypes } from './write.js';
/**
* These should be ignored in the context of GraphQL, and/or are replaced by a custom resolver (for non-standard structures)
*/
export const SYSTEM_DENY_LIST = [
'directus_collections',
'directus_fields',
'directus_relations',
'directus_migrations',
'directus_sessions',
'directus_extensions',
];
export const READ_ONLY = ['directus_activity', 'directus_revisions'];
const env = useEnv();
const semaphore = new Semaphore(env['GRAPHQL_SCHEMA_GENERATION_MAX_CONCURRENT'] ?? 5);
/**
* Generate the GraphQL schema. Pulls from the schema information generated by the get-schema util.
*/
export async function generateSchema(gql, type = 'schema') {
const key = `${gql.scope}_${type}_${gql.accountability?.role}_${gql.accountability?.user}`;
const cachedSchema = cache.get(key);
if (cachedSchema)
return cachedSchema;
return semaphore.runExclusive(async () => {
// Check the cache again after acquiring the lock
const cachedSchema = cache.get(key);
if (cachedSchema)
return cachedSchema;
const schemaComposer = new SchemaComposer();
let schema;
const sanitizedSchema = sanitizeGraphqlSchema(gql.schema);
if (!gql.accountability || gql.accountability.admin) {
schema = {
read: sanitizedSchema,
create: sanitizedSchema,
update: sanitizedSchema,
delete: sanitizedSchema,
};
}
else {
schema = {
read: reduceSchema(sanitizedSchema, await fetchAllowedFieldMap({
accountability: gql.accountability,
action: 'read',
}, { schema: gql.schema, knex: gql.knex })),
create: reduceSchema(sanitizedSchema, await fetchAllowedFieldMap({
accountability: gql.accountability,
action: 'create',
}, { schema: gql.schema, knex: gql.knex })),
update: reduceSchema(sanitizedSchema, await fetchAllowedFieldMap({
accountability: gql.accountability,
action: 'update',
}, { schema: gql.schema, knex: gql.knex })),
delete: reduceSchema(sanitizedSchema, await fetchAllowedFieldMap({
accountability: gql.accountability,
action: 'delete',
}, { schema: gql.schema, knex: gql.knex })),
};
}
const inconsistentFields = {
read: await fetchInconsistentFieldMap({
accountability: gql.accountability,
action: 'read',
}, { schema: gql.schema, knex: gql.knex }),
create: await fetchInconsistentFieldMap({
accountability: gql.accountability,
action: 'create',
}, { schema: gql.schema, knex: gql.knex }),
update: await fetchInconsistentFieldMap({
accountability: gql.accountability,
action: 'update',
}, { schema: gql.schema, knex: gql.knex }),
delete: await fetchInconsistentFieldMap({
accountability: gql.accountability,
action: 'delete',
}, { schema: gql.schema, knex: gql.knex }),
};
const { ReadCollectionTypes, VersionCollectionTypes } = await getReadableTypes(gql, schemaComposer, schema, inconsistentFields);
const { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes } = getWritableTypes(gql, schemaComposer, schema, inconsistentFields, ReadCollectionTypes);
const CollectionTypes = {
CreateCollectionTypes,
ReadCollectionTypes,
UpdateCollectionTypes,
DeleteCollectionTypes,
};
const scopeFilter = (collection) => {
if (gql.scope === 'items' && isSystemCollection(collection.collection))
return false;
if (gql.scope === 'system') {
if (isSystemCollection(collection.collection) === false)
return false;
if (SYSTEM_DENY_LIST.includes(collection.collection))
return false;
}
return true;
};
if (gql.scope === 'system') {
injectSystemResolvers(gql, schemaComposer, CollectionTypes, 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 = gql.scope === 'items' ? collection.collection : collection.collection.substring(9);
acc[collectionName] = ReadCollectionTypes[collection.collection].getResolver(collection.collection);
if (gql.schema.collections[collection.collection].singleton === false) {
acc[`${collectionName}_by_id`] = ReadCollectionTypes[collection.collection].getResolver(`${collection.collection}_by_id`);
acc[`${collectionName}_aggregated`] = ReadCollectionTypes[collection.collection].getResolver(`${collection.collection}_aggregated`);
}
if (gql.scope === 'items') {
acc[`${collectionName}_by_version`] = VersionCollectionTypes[collection.collection].getResolver(`${collection.collection}_by_version`);
}
return acc;
}, {}));
}
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 = gql.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;
}, {}));
}
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 = gql.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}_batch`] = UpdateCollectionTypes[collection.collection].getResolver(`update_${collection.collection}_batch`);
acc[`update_${collectionName}_item`] = UpdateCollectionTypes[collection.collection].getResolver(`update_${collection.collection}_item`);
}
return acc;
}, {}));
}
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 = gql.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;
}, {}));
}
if (type === 'sdl') {
const sdl = schemaComposer.toSDL();
cache.set(key, sdl);
return sdl;
}
const gqlSchema = schemaComposer.buildSchema();
cache.set(key, gqlSchema);
return gqlSchema;
});
}