@datocms/cma-client
Version:
JS client for DatoCMS REST Content Management API
664 lines • 29.7 kB
JavaScript
"use strict";
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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaRepository = void 0;
const rest_client_utils_1 = require("@datocms/rest-client-utils");
const fieldsContainingReferences_1 = require("./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
*/
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 (0, rest_client_utils_1.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 (0, rest_client_utils_1.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 (0, rest_client_utils_1.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 (0, rest_client_utils_1.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 (0, rest_client_utils_1.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 (0, rest_client_utils_1.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 (0, rest_client_utils_1.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 (0, rest_client_utils_1.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 (0, rest_client_utils_1.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 (0, rest_client_utils_1.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 = (0, fieldsContainingReferences_1.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 (0, rest_client_utils_1.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 = (0, fieldsContainingReferences_1.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 (0, rest_client_utils_1.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 = (0, fieldsContainingReferences_1.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 = (0, fieldsContainingReferences_1.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 (0, rest_client_utils_1.deserializeResponseBody)({
data: rawResult,
});
});
}
}
exports.SchemaRepository = SchemaRepository;
//# sourceMappingURL=schemaRepository.js.map