@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
176 lines (174 loc) • 8.45 kB
JavaScript
import { reduceSchema } from "../../../utils/reduce-schema.js";
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 { GraphQLVoid } from "../types/void.js";
import { injectSystemResolvers } from "../resolvers/system.js";
import { cache } from "../schema-cache.js";
import { sanitizeGraphqlSchema } from "../utils/sanitize-gql-schema.js";
import { getReadableTypes } from "./read.js";
import { getWritableTypes } from "./write.js";
import "../index.js";
import { useEnv } from "@directus/env";
import { isSystemCollection } from "@directus/system-data";
import "graphql";
import { Semaphore } from "async-mutex";
import { SchemaComposer } from "graphql-compose";
//#region src/services/graphql/schema/index.ts
/**
* 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",
"directus_extensions"
];
const READ_ONLY = ["directus_activity", "directus_revisions"];
const semaphore = new Semaphore(useEnv()["GRAPHQL_SCHEMA_GENERATION_MAX_CONCURRENT"] ?? 5);
/**
* Generate the GraphQL schema. Pulls from the schema information generated by the get-schema util.
*/
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 () => {
const cachedSchema$1 = cache.get(key);
if (cachedSchema$1) return cachedSchema$1;
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;
});
}
//#endregion
export { READ_ONLY, SYSTEM_DENY_LIST, generateSchema };