@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
152 lines (150 loc) • 6.26 kB
JavaScript
import { useBus } from "../bus/lib/use-bus.js";
import "../bus/index.js";
import { useLogger } from "../logger/index.js";
import { ALIAS_TYPES } from "../constants.js";
import { getMemorySchemaCache, setMemorySchemaCache } from "../cache.js";
import database_default from "../database/index.js";
import { useLock } from "../lock/lib/use-lock.js";
import "../lock/index.js";
import { RelationsService } from "../services/relations.js";
import getLocalType from "./get-local-type.js";
import getDefaultValue from "./get-default-value.js";
import { getSystemFieldRowsWithAuthProviders } from "./get-field-system-rows.js";
import { useEnv } from "@directus/env";
import { parseJSON, toArray, toBoolean } from "@directus/utils";
import { mapValues, pick } from "lodash-es";
import { createInspector } from "@directus/schema";
import { systemCollectionRows } from "@directus/system-data";
//#region src/utils/get-schema.ts
const logger = useLogger();
async function getSchema(options, attempt = 0) {
const MAX_ATTEMPTS = 3;
const env = useEnv();
if (options?.bypassCache || env["CACHE_SCHEMA"] === false) {
const database = options?.database || database_default();
return await getDatabaseSchema(database, createInspector(database));
}
const cached = getMemorySchemaCache();
if (cached) return cached;
if (attempt >= MAX_ATTEMPTS) throw new Error(`Failed to get Schema information: hit infinite loop`);
const lock = useLock();
const bus = useBus();
const lockKey = "schemaCache--preparing";
const messageKey = "schemaCache--done";
const processId = await lock.increment(lockKey);
if (processId >= env["CACHE_SCHEMA_MAX_ITERATIONS"]) await lock.delete(lockKey);
if (processId === 1 === false) {
logger.trace("Schema cache is prepared in another process, waiting for result.");
const timeout = new Promise((_, reject) => setTimeout(reject, env["CACHE_SCHEMA_SYNC_TIMEOUT"]));
const subscription = new Promise((resolve, reject) => {
bus.subscribe(messageKey, busListener).catch(reject);
function busListener(options$1) {
cleanup();
if (options$1.schema === null) return reject();
try {
setMemorySchemaCache(options$1.schema);
resolve(options$1.schema);
} catch (e) {
reject(e);
}
}
function cleanup() {
bus.unsubscribe(messageKey, busListener).catch(reject);
}
});
return Promise.race([timeout, subscription]).catch(() => getSchema(options, attempt + 1));
}
let schema = null;
try {
const database = options?.database || database_default();
schema = await getDatabaseSchema(database, createInspector(database));
setMemorySchemaCache(schema);
return schema;
} finally {
await bus.publish(messageKey, { schema });
await lock.delete(lockKey);
}
}
async function getDatabaseSchema(database, schemaInspector) {
const env = useEnv();
const result = {
collections: {},
relations: []
};
const systemFieldRows$1 = getSystemFieldRowsWithAuthProviders();
const schemaOverview = await schemaInspector.overview();
const collections = [...(await database.select("*").from("directus_collections")).filter((c) => !("status" in c) || c["status"] === "active").map((c) => pick(c, "collection", "singleton", "note", "sort_field", "accountability")), ...systemCollectionRows];
for (const [collection, info] of Object.entries(schemaOverview)) {
if (toArray(env["DB_EXCLUDE_TABLES"]).includes(collection)) {
logger.trace(`Collection "${collection}" is configured to be excluded and will be ignored`);
continue;
}
if (!info.primary) {
logger.warn(`Collection "${collection}" doesn't have a primary key column and will be ignored`);
continue;
}
if (collection.includes(" ")) {
logger.warn(`Collection "${collection}" has a space in the name and will be ignored`);
continue;
}
const collectionMeta = collections.find((collectionMeta$1) => collectionMeta$1.collection === collection);
result.collections[collection] = {
collection,
primary: info.primary,
singleton: toBoolean(collectionMeta?.singleton),
note: collectionMeta?.note || null,
sortField: collectionMeta?.sort_field || null,
accountability: collectionMeta ? collectionMeta.accountability : "all",
fields: mapValues(schemaOverview[collection]?.columns, (column) => {
return {
field: column.column_name,
defaultValue: getDefaultValue(column) ?? null,
nullable: column.is_nullable ?? true,
generated: column.is_generated ?? false,
type: getLocalType(column),
dbType: column.data_type,
precision: column.numeric_precision || null,
scale: column.numeric_scale || null,
special: [],
note: null,
validation: null,
alias: false,
searchable: true
};
})
};
}
const fields = [...await database.select("id", "collection", "field", "special", "note", "validation", "searchable").from("directus_fields"), ...systemFieldRows$1].filter((field) => (field.special ? toArray(field.special) : []).includes("no-data") === false);
for (const field of fields) {
if (!result.collections[field.collection]) continue;
const existing = result.collections[field.collection]?.fields[field.field];
const column = schemaOverview[field.collection]?.columns[field.field];
const special = field.special ? toArray(field.special) : [];
if (ALIAS_TYPES.some((type$1) => special.includes(type$1)) === false && !existing) continue;
const type = existing && getLocalType(column, { special }) || "alias";
let validation = field.validation ?? null;
if (validation && typeof validation === "string") validation = parseJSON(validation);
result.collections[field.collection].fields[field.field] = {
field: field.field,
defaultValue: existing?.defaultValue ?? null,
nullable: existing?.nullable ?? true,
generated: existing?.generated ?? false,
type,
dbType: existing?.dbType || null,
precision: existing?.precision || null,
scale: existing?.scale || null,
special,
note: field.note,
alias: existing?.alias ?? true,
validation: validation ?? null,
searchable: toBoolean(field.searchable) ?? true
};
}
result.relations = await new RelationsService({
knex: database,
schema: result
}).readAll(void 0, void 0, true);
return result;
}
//#endregion
export { getSchema };