UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

191 lines (190 loc) 10.2 kB
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; }); }