UNPKG

@datocms/cma-client

Version:
660 lines 29.1 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { deserializeResponseBody } from '@datocms/rest-client-utils'; import { blockModelIdsReferencedInField, modelIdsReferencedInField, } from './fieldsContainingReferences'; /** * Repository for DatoCMS schema entities including item types, fields, and plugins. * Provides caching and efficient lookup functionality for schema-related operations. * * ## Purpose * * SchemaRepository is designed to solve the performance problem of repeatedly fetching * the same schema information during complex operations that traverse nested blocks, * structured text, or modular content. It acts as an in-memory cache/memoization layer * for schema entities to avoid redundant API calls. * * ## What it's for: * * - **Caching schema entities**: Automatically caches item types, fields, fieldsets, * and plugins after the first API request, returning cached results on subsequent calls * - **Complex traversal operations**: Essential when using utilities like * `mapBlocksInNonLocalizedFieldValue()` that need to repeatedly lookup block models and fields * while traversing nested content structures * - **Bulk operations**: Ideal for scripts that process multiple records of different * types and need efficient access to schema information * - **Read-heavy workflows**: Perfect for scenarios where you need to repeatedly access * the same schema information without making redundant API calls * * ## What it's NOT for: * * - **Schema modification**: Do NOT use SchemaRepository if your script modifies models, * fields, fieldsets, or plugins, as the cache will become stale * - **Record/content operations**: This is only for schema entities, not for records, * uploads, or other content * - **Long-running applications**: The cache has no expiration or invalidation mechanism, * so it's not suitable for applications that need fresh schema data over time * - **Concurrent schema changes**: No protection against cache inconsistency if other * processes modify the schema while your script runs * * ## Usage Pattern * * ```typescript * const schemaRepository = new SchemaRepository(client); * * // These calls will hit the API and cache the results * const models = await schemaRepository.getAllModels(); * const blogPost = await schemaRepository.getItemTypeByApiKey('blog_post'); * * // These subsequent calls will return cached results (no API calls) * const sameModels = await schemaRepository.getAllModels(); * const sameBlogPost = await schemaRepository.getItemTypeByApiKey('blog_post'); * * // Pass the repository to utilities that need schema information * await mapBlocksInNonLocalizedFieldValue(schemaRepository, record, (block) => { * // The utility will use the cached schema data internally * }); * ``` * * ## Performance Benefits * * Without SchemaRepository, a script processing structured text with nested blocks * might make the same `client.itemTypes.list()` or `client.fields.list()` calls * dozens of times. SchemaRepository ensures each unique schema request is made only once. * * ## Best Practices * * - Use SchemaRepository consistently throughout your script — if you need it for one * utility call, use it for all schema operations to maximize cache efficiency * - Create one instance per script execution, not per operation * - Only use when you have read-only access to schema entities * - Consider using optimistic locking for any record updates to handle potential * version conflicts when working with cached schema information */ export class SchemaRepository { /** * Creates a new SchemaRepository instance. * @param client - The DatoCMS client instance */ constructor(client) { this.itemTypesPromise = null; this.itemTypesByApiKey = new Map(); this.itemTypesById = new Map(); this.fieldsByItemType = new Map(); this.fieldsetsByItemType = new Map(); this.pluginsPromise = null; this.pluginsById = new Map(); this.pluginsByPackageName = new Map(); this.prefetchPromise = null; this.client = client; } /** * Loads and caches all item types from the DatoCMS API. * This method is called lazily and caches the result for subsequent calls. * @returns Promise that resolves to an array of item types */ loadItemTypes() { return __awaiter(this, void 0, void 0, function* () { if (!this.itemTypesPromise) { this.itemTypesPromise = (() => __awaiter(this, void 0, void 0, function* () { const { data: itemTypes } = yield this.client.itemTypes.rawList(); // Populate the lookup maps for (const itemType of itemTypes) { this.itemTypesByApiKey.set(itemType.attributes.api_key, itemType); this.itemTypesById.set(itemType.id, itemType); } return itemTypes; }))(); } return this.itemTypesPromise; }); } /** * Gets all item types from the DatoCMS project. * @returns Promise that resolves to an array of all item types */ getAllItemTypes() { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getAllRawItemTypes(); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets all item types from the DatoCMS project. * @returns Promise that resolves to an array of all item types */ getAllRawItemTypes() { return __awaiter(this, void 0, void 0, function* () { const itemTypes = yield this.loadItemTypes(); return itemTypes; }); } /** * Gets all item types that are models (not modular blocks). * @returns Promise that resolves to an array of model item types */ getAllModels() { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getAllRawModels(); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets all item types that are models (not modular blocks). * @returns Promise that resolves to an array of model item types */ getAllRawModels() { return __awaiter(this, void 0, void 0, function* () { const itemTypes = yield this.loadItemTypes(); return itemTypes.filter((it) => !it.attributes.modular_block); }); } /** * Gets all item types that are modular blocks. * @returns Promise that resolves to an array of block model item types */ getAllBlockModels() { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getAllRawBlockModels(); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets all item types that are modular blocks. * @returns Promise that resolves to an array of block model item types */ getAllRawBlockModels() { return __awaiter(this, void 0, void 0, function* () { const itemTypes = yield this.loadItemTypes(); return itemTypes.filter((it) => it.attributes.modular_block); }); } /** * Gets an item type by its API key. * @param apiKey - The API key of the item type to retrieve * @returns Promise that resolves to the item type * @throws Error if the item type is not found */ getItemTypeByApiKey(apiKey) { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getRawItemTypeByApiKey(apiKey); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets an item type by its API key. * @param apiKey - The API key of the item type to retrieve * @returns Promise that resolves to the item type * @throws Error if the item type is not found */ getRawItemTypeByApiKey(apiKey) { return __awaiter(this, void 0, void 0, function* () { yield this.loadItemTypes(); const itemType = this.itemTypesByApiKey.get(apiKey); if (!itemType) { throw new Error(`Item type with API key '${apiKey}' not found`); } return itemType; }); } /** * Gets an item type by its ID. * @param id - The ID of the item type to retrieve * @returns Promise that resolves to the item type * @throws Error if the item type is not found */ getItemTypeById(id) { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getRawItemTypeById(id); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets an item type by its ID. * @param id - The ID of the item type to retrieve * @returns Promise that resolves to the item type * @throws Error if the item type is not found */ getRawItemTypeById(id) { return __awaiter(this, void 0, void 0, function* () { yield this.loadItemTypes(); const itemType = this.itemTypesById.get(id); if (!itemType) { throw new Error(`Item type with ID '${id}' not found`); } return itemType; }); } /** * Gets all fields for a given item type. * Fields are cached after the first request for performance. * @param itemType - The item type to get fields for * @returns Promise that resolves to an array of fields */ getItemTypeFields(itemType) { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getRawItemTypeFields(itemType); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets all fields for a given item type. * Fields are cached after the first request for performance. * @param itemType - The item type to get fields for * @returns Promise that resolves to an array of fields */ getRawItemTypeFields(itemType) { return __awaiter(this, void 0, void 0, function* () { // Check if we already have the fields cached const cachedFields = this.fieldsByItemType.get(itemType.id); if (cachedFields) { return cachedFields; } // Fetch and cache the fields const { data: fields } = yield this.client.fields.rawList(itemType.id); this.fieldsByItemType.set(itemType.id, fields); return fields; }); } /** * Gets all fieldsets for a given item type. * Fieldsets are cached after the first request for performance. * @param itemType - The item type to get fieldsets for * @returns Promise that resolves to an array of fieldsets */ getItemTypeFieldsets(itemType) { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getRawItemTypeFieldsets(itemType); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets all fieldsets for a given item type. * Fieldsets are cached after the first request for performance. * @param itemType - The item type to get fieldsets for * @returns Promise that resolves to an array of fieldsets */ getRawItemTypeFieldsets(itemType) { return __awaiter(this, void 0, void 0, function* () { // Check if we already have the fieldsets cached const cachedFieldsets = this.fieldsetsByItemType.get(itemType.id); if (cachedFieldsets) { return cachedFieldsets; } // Fetch and cache the fieldsets const { data: fieldsets } = yield this.client.fieldsets.rawList(itemType.id); this.fieldsetsByItemType.set(itemType.id, fieldsets); return fieldsets; }); } /** * Loads and caches all plugins from the DatoCMS API. * This method is called lazily and caches the result for subsequent calls. * @returns Promise that resolves to an array of plugins */ loadPlugins() { return __awaiter(this, void 0, void 0, function* () { if (!this.pluginsPromise) { this.pluginsPromise = (() => __awaiter(this, void 0, void 0, function* () { const { data: plugins } = yield this.client.plugins.rawList(); // Populate the lookup maps for (const plugin of plugins) { this.pluginsById.set(plugin.id, plugin); if (plugin.attributes.package_name) { this.pluginsByPackageName.set(plugin.attributes.package_name, plugin); } } return plugins; }))(); } return this.pluginsPromise; }); } /** * Gets all plugins from the DatoCMS project. * @returns Promise that resolves to an array of all plugins */ getAllPlugins() { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getAllRawPlugins(); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets all plugins from the DatoCMS project. * @returns Promise that resolves to an array of all plugins */ getAllRawPlugins() { return __awaiter(this, void 0, void 0, function* () { const plugins = yield this.loadPlugins(); return plugins; }); } /** * Gets a plugin by its ID. * @param id - The ID of the plugin to retrieve * @returns Promise that resolves to the plugin * @throws Error if the plugin is not found */ getPluginById(id) { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getRawPluginById(id); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets a plugin by its ID. * @param id - The ID of the plugin to retrieve * @returns Promise that resolves to the plugin * @throws Error if the plugin is not found */ getRawPluginById(id) { return __awaiter(this, void 0, void 0, function* () { yield this.loadPlugins(); const plugin = this.pluginsById.get(id); if (!plugin) { throw new Error(`Plugin with ID '${id}' not found`); } return plugin; }); } /** * Gets a plugin by its package name. * @param packageName - The package name of the plugin to retrieve * @returns Promise that resolves to the plugin * @throws Error if the plugin is not found */ getPluginByPackageName(packageName) { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getRawPluginByPackageName(packageName); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets a plugin by its package name. * @param packageName - The package name of the plugin to retrieve * @returns Promise that resolves to the plugin * @throws Error if the plugin is not found */ getRawPluginByPackageName(packageName) { return __awaiter(this, void 0, void 0, function* () { yield this.loadPlugins(); const plugin = this.pluginsByPackageName.get(packageName); if (!plugin) { throw new Error(`Plugin with package name '${packageName}' not found`); } return plugin; }); } /** * Prefetches all models and their fields in a single optimized API call. * This method populates the internal caches for both item types and fields, * making subsequent lookups very fast without additional API calls. * * This is more efficient than lazy loading when you know you'll need access * to multiple models and fields, as it reduces the number of API requests * from potentially dozens down to just one. * * @returns Promise that resolves when all data has been fetched and cached */ prefetchAllModelsAndFields() { return __awaiter(this, void 0, void 0, function* () { if (this.prefetchPromise) { return this.prefetchPromise; } const prefetch = () => __awaiter(this, void 0, void 0, function* () { const { included } = yield this.client.site.rawFind({ include: 'item_types,item_types.fields', }); if (!included) { return; } const allItemTypes = included.filter((item) => item.type === 'item_type'); const allFields = included.filter((item) => item.type === 'field'); // Populate item types caches this.itemTypesPromise = Promise.resolve(allItemTypes); for (const itemType of allItemTypes) { this.itemTypesByApiKey.set(itemType.attributes.api_key, itemType); this.itemTypesById.set(itemType.id, itemType); } // Group fields by item type and populate fields cache const fieldsByItemTypeId = new Map(); for (const field of allFields) { const itemTypeId = field.relationships.item_type.data.id; if (!fieldsByItemTypeId.has(itemTypeId)) { fieldsByItemTypeId.set(itemTypeId, []); } fieldsByItemTypeId.get(itemTypeId).push(field); } // Populate the fields cache for (const [itemTypeId, fields] of fieldsByItemTypeId) { this.fieldsByItemType.set(itemTypeId, fields); } }); this.prefetchPromise = prefetch(); return this.prefetchPromise; }); } /** * Gets all models that directly or indirectly embed the given block models. * This method recursively traverses the schema to find all models that reference * the provided blocks, either directly through block fields or indirectly through * other block models that reference them. * * @param blocks - Array of block models to find references to * @returns Promise that resolves to array of models that embed these blocks */ getRawModelsEmbeddingBlocks(blocks) { return __awaiter(this, void 0, void 0, function* () { yield this.prefetchAllModelsAndFields(); const allItemTypes = yield this.getAllRawItemTypes(); const blockIds = new Set(blocks.map((block) => block.id)); const embeddingModels = []; // Helper function to check if a model points to any of the target blocks const modelPointsToBlocks = (itemType, alreadyExplored = new Set()) => __awaiter(this, void 0, void 0, function* () { if (alreadyExplored.has(itemType.id)) { return false; } alreadyExplored.add(itemType.id); const fields = yield this.getRawItemTypeFields(itemType); for (const field of fields) { const referencedBlockIds = blockModelIdsReferencedInField(field); // Check if this field directly references any of our target blocks if (referencedBlockIds.some((id) => blockIds.has(id))) { return true; } // Check if this field references other block models that might transitively reference our targets const referencedBlocks = referencedBlockIds.map((id) => allItemTypes.find((it) => it.id === id)); for (const linkedBlock of referencedBlocks) { if (yield modelPointsToBlocks(linkedBlock, new Set(alreadyExplored))) { return true; } } } return false; }); // Check each model to see if it embeds any of the target blocks for (const itemType of allItemTypes) { if (yield modelPointsToBlocks(itemType)) { embeddingModels.push(itemType); } } return embeddingModels; }); } /** * Gets all models that directly or indirectly embed the given block models. * This method recursively traverses the schema to find all models that reference * the provided blocks, either directly through block fields or indirectly through * other block models that reference them. * * @param blocks - Array of block models to find references to * @returns Promise that resolves to array of models that embed these blocks */ getModelsEmbeddingBlocks(blocks) { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getRawModelsEmbeddingBlocks(blocks); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets all block models that are directly or indirectly nested within the given item types. * This method recursively traverses the schema to find all blocks that are nested * within the provided item types, either directly through block fields or indirectly through * other nested block models. * * @param itemTypes - Array of item types to find nested blocks for * @returns Promise that resolves to array of all block models nested in these item types */ getRawNestedBlocks(itemTypes) { return __awaiter(this, void 0, void 0, function* () { yield this.prefetchAllModelsAndFields(); const allItemTypes = yield this.getAllRawItemTypes(); const visited = new Set(); const nestedBlocks = []; // Helper function to recursively find nested blocks const findNestedBlocks = (itemType, alreadyExplored = new Set()) => __awaiter(this, void 0, void 0, function* () { if (alreadyExplored.has(itemType.id)) { return; } alreadyExplored.add(itemType.id); const fields = yield this.getRawItemTypeFields(itemType); for (const field of fields) { const referencedBlockIds = blockModelIdsReferencedInField(field); for (const blockId of referencedBlockIds) { if (!visited.has(blockId)) { visited.add(blockId); const nestedBlock = allItemTypes.find((it) => it.id === blockId); if (nestedBlock) { nestedBlocks.push(nestedBlock); // Recursively find blocks nested in this block yield findNestedBlocks(nestedBlock, new Set(alreadyExplored)); } } } } }); // Find nested blocks for each provided item type for (const itemType of itemTypes) { yield findNestedBlocks(itemType); } return nestedBlocks; }); } /** * Gets all block models that are directly or indirectly nested within the given item types. * This method recursively traverses the schema to find all blocks that are nested * within the provided item types, either directly through block fields or indirectly through * other nested block models. * * @param itemTypes - Array of item types to find nested blocks for * @returns Promise that resolves to array of all block models nested in these item types */ getNestedBlocks(itemTypes) { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getRawNestedBlocks(itemTypes); return deserializeResponseBody({ data: rawResult, }); }); } /** * Gets all models that are directly or indirectly nested/referenced within the given item types. * This method recursively traverses the schema to find all models that are referenced * by the provided item types through link fields, either directly or indirectly through * other referenced blocks. * * @param itemTypes - Array of item types to find nested models for * @returns Promise that resolves to array of all models nested in these item types */ getRawNestedModels(itemTypes) { return __awaiter(this, void 0, void 0, function* () { yield this.prefetchAllModelsAndFields(); const allItemTypes = yield this.getAllRawItemTypes(); const visited = new Set(); const nestedModels = []; // Helper function to recursively find nested models const findNestedModels = (itemType, alreadyExplored = new Set()) => __awaiter(this, void 0, void 0, function* () { if (alreadyExplored.has(itemType.id)) { return; } alreadyExplored.add(itemType.id); const fields = yield this.getRawItemTypeFields(itemType); for (const field of fields) { // Find models directly referenced via link fields const referencedModelIds = modelIdsReferencedInField(field); for (const modelId of referencedModelIds) { if (!visited.has(modelId)) { visited.add(modelId); const nestedModel = allItemTypes.find((it) => it.id === modelId); if (nestedModel) { nestedModels.push(nestedModel); // Do NOT recurse into models, only into blocks } } } // Find blocks referenced via block fields, then recursively find models in those blocks const referencedBlockIds = blockModelIdsReferencedInField(field); for (const blockId of referencedBlockIds) { const nestedBlock = allItemTypes.find((it) => it.id === blockId); if (nestedBlock) { // Recursively find models nested in this block yield findNestedModels(nestedBlock, new Set(alreadyExplored)); } } } }); // Find nested models for each provided item type for (const itemType of itemTypes) { yield findNestedModels(itemType); } return nestedModels; }); } /** * Gets all models that are directly or indirectly nested/referenced within the given item types. * This method recursively traverses the schema to find all models that are referenced * by the provided item types through link fields, either directly or indirectly through * other referenced blocks. * * @param itemTypes - Array of item types to find nested models for * @returns Promise that resolves to array of all models nested in these item types */ getNestedModels(itemTypes) { return __awaiter(this, void 0, void 0, function* () { const rawResult = yield this.getRawNestedModels(itemTypes); return deserializeResponseBody({ data: rawResult, }); }); } } //# sourceMappingURL=schemaRepository.js.map