directus-task-management/scripts/utils/schema-cache.js

471 lines
12 KiB
JavaScript

/**
* Schema Cache Manager
* Maintains an offline cache of Directus collections and fields schema
* Updates cache on every create/update/delete operation
*/
const fs = require('fs').promises;
const path = require('path');
const {
readCollections,
readFields,
readRelations
} = require('@directus/sdk');
class SchemaCache {
constructor(cacheDir = null) {
this.cacheDir = cacheDir || path.join(process.cwd(), '.directus-cache');
this.cacheFile = path.join(this.cacheDir, 'schema.json');
this.lockFile = path.join(this.cacheDir, 'schema.lock');
this.cache = null;
this.isDirty = false;
}
/**
* Initialize cache directory and files
*/
async init() {
// Skip if already initialized
if (this.cache !== null) {
return true;
}
try {
// Create cache directory if it doesn't exist
await fs.mkdir(this.cacheDir, { recursive: true });
// Try to load existing cache
await this.load();
return true;
} catch (error) {
console.error('❌ Failed to initialize cache:', error.message);
return false;
}
}
/**
* Load cache from disk
*/
async load() {
try {
const data = await fs.readFile(this.cacheFile, 'utf-8');
this.cache = JSON.parse(data);
return this.cache;
} catch (error) {
// If file doesn't exist, initialize empty cache
if (error.code === 'ENOENT') {
this.cache = {
version: '1.0.0',
lastSync: null,
collections: {},
relations: [],
metadata: {
totalCollections: 0,
totalFields: 0,
totalRelations: 0
}
};
await this.save();
} else {
throw error;
}
}
return this.cache;
}
/**
* Save cache to disk
*/
async save() {
try {
// Add timestamp
this.cache.lastModified = new Date().toISOString();
// Update metadata
this.updateMetadata();
// Write with pretty formatting for readability
const data = JSON.stringify(this.cache, null, 2);
await fs.writeFile(this.cacheFile, data, 'utf-8');
this.isDirty = false;
return true;
} catch (error) {
console.error('❌ Failed to save cache:', error.message);
return false;
}
}
/**
* Update cache metadata
*/
updateMetadata() {
if (!this.cache) return;
const collections = Object.keys(this.cache.collections);
let totalFields = 0;
collections.forEach(collection => {
if (this.cache.collections[collection].fields) {
totalFields += Object.keys(this.cache.collections[collection].fields).length;
}
});
this.cache.metadata = {
totalCollections: collections.length,
totalFields: totalFields,
totalRelations: this.cache.relations ? this.cache.relations.length : 0
};
}
/**
* Sync cache with Directus instance
*/
async sync(client) {
try {
console.log('🔄 Syncing schema cache with Directus...');
// Fetch all collections
const collections = await client.request(readCollections());
// Reset cache
this.cache = {
version: '1.0.0',
lastSync: new Date().toISOString(),
collections: {},
relations: [],
metadata: {}
};
// Process each collection
for (const collection of collections) {
// Skip system collections unless specified
if (collection.collection.startsWith('directus_') && !collection.meta?.system) {
continue;
}
// Store collection info
this.cache.collections[collection.collection] = {
name: collection.collection,
meta: collection.meta,
schema: collection.schema,
fields: {},
created: new Date().toISOString()
};
// Fetch fields for this collection
try {
const fields = await client.request(readFields(collection.collection));
// Store field info
fields.forEach(field => {
this.cache.collections[collection.collection].fields[field.field] = {
field: field.field,
type: field.type,
meta: field.meta,
schema: field.schema
};
});
} catch (fieldError) {
console.warn(` ⚠️ Could not fetch fields for ${collection.collection}:`, fieldError.message);
}
}
// Fetch all relations
try {
const relations = await client.request(readRelations());
this.cache.relations = relations;
} catch (relError) {
console.warn(' ⚠️ Could not fetch relations:', relError.message);
}
// Save to disk
await this.save();
console.log(`✅ Synced ${Object.keys(this.cache.collections).length} collections`);
console.log(`📊 Cache statistics:`, this.cache.metadata);
return this.cache;
} catch (error) {
console.error('❌ Failed to sync cache:', error.message);
throw error;
}
}
/**
* Check if a collection exists in cache
*/
hasCollection(collectionName) {
return this.cache &&
this.cache.collections &&
this.cache.collections[collectionName] !== undefined;
}
/**
* Check if a field exists in cache
*/
hasField(collectionName, fieldName) {
return this.hasCollection(collectionName) &&
this.cache.collections[collectionName].fields &&
this.cache.collections[collectionName].fields[fieldName] !== undefined;
}
/**
* Get collection info from cache
*/
getCollection(collectionName) {
if (!this.hasCollection(collectionName)) {
return null;
}
return this.cache.collections[collectionName];
}
/**
* Get field info from cache
*/
getField(collectionName, fieldName) {
if (!this.hasField(collectionName, fieldName)) {
return null;
}
return this.cache.collections[collectionName].fields[fieldName];
}
/**
* Add or update collection in cache
*/
async addCollection(collectionName, collectionData = {}) {
if (!this.cache) await this.load();
this.cache.collections[collectionName] = {
name: collectionName,
meta: collectionData.meta || {},
schema: collectionData.schema || {},
fields: collectionData.fields || {},
created: collectionData.created || new Date().toISOString(),
modified: new Date().toISOString()
};
this.isDirty = true;
await this.save();
return this.cache.collections[collectionName];
}
/**
* Add or update field in cache
*/
async addField(collectionName, fieldName, fieldData = {}) {
if (!this.cache) await this.load();
// Ensure collection exists
if (!this.hasCollection(collectionName)) {
await this.addCollection(collectionName);
}
// Ensure fields object exists
if (!this.cache.collections[collectionName].fields) {
this.cache.collections[collectionName].fields = {};
}
// Add or update field
this.cache.collections[collectionName].fields[fieldName] = {
field: fieldName,
type: fieldData.type,
meta: fieldData.meta || {},
schema: fieldData.schema || {},
created: fieldData.created || new Date().toISOString(),
modified: new Date().toISOString()
};
this.isDirty = true;
await this.save();
return this.cache.collections[collectionName].fields[fieldName];
}
/**
* Remove collection from cache
*/
async removeCollection(collectionName) {
if (!this.cache) await this.load();
if (this.hasCollection(collectionName)) {
delete this.cache.collections[collectionName];
this.isDirty = true;
await this.save();
return true;
}
return false;
}
/**
* Remove field from cache
*/
async removeField(collectionName, fieldName) {
if (!this.cache) await this.load();
if (this.hasField(collectionName, fieldName)) {
delete this.cache.collections[collectionName].fields[fieldName];
this.isDirty = true;
await this.save();
return true;
}
return false;
}
/**
* Rename field in cache
*/
async renameField(collectionName, oldFieldName, newFieldName) {
if (!this.cache) await this.load();
if (this.hasField(collectionName, oldFieldName)) {
const fieldData = this.cache.collections[collectionName].fields[oldFieldName];
fieldData.field = newFieldName;
fieldData.modified = new Date().toISOString();
fieldData.previousName = oldFieldName;
// Add new field name
this.cache.collections[collectionName].fields[newFieldName] = fieldData;
// Remove old field name
delete this.cache.collections[collectionName].fields[oldFieldName];
this.isDirty = true;
await this.save();
return true;
}
return false;
}
/**
* Add or update relation in cache
*/
async addRelation(relationData) {
if (!this.cache) await this.load();
if (!this.cache.relations) {
this.cache.relations = [];
}
// Check if relation already exists
const existingIndex = this.cache.relations.findIndex(r =>
r.collection === relationData.collection &&
r.field === relationData.field
);
if (existingIndex >= 0) {
// Update existing relation
this.cache.relations[existingIndex] = {
...relationData,
modified: new Date().toISOString()
};
} else {
// Add new relation
this.cache.relations.push({
...relationData,
created: new Date().toISOString()
});
}
this.isDirty = true;
await this.save();
return true;
}
/**
* Get cache statistics
*/
getStats() {
if (!this.cache) {
return {
initialized: false
};
}
const collections = Object.keys(this.cache.collections);
let totalFields = 0;
let systemCollections = 0;
let customCollections = 0;
collections.forEach(collection => {
if (collection.startsWith('directus_')) {
systemCollections++;
} else {
customCollections++;
}
if (this.cache.collections[collection].fields) {
totalFields += Object.keys(this.cache.collections[collection].fields).length;
}
});
return {
initialized: true,
lastSync: this.cache.lastSync,
lastModified: this.cache.lastModified,
totalCollections: collections.length,
systemCollections,
customCollections,
totalFields,
totalRelations: this.cache.relations ? this.cache.relations.length : 0,
cacheFile: this.cacheFile
};
}
/**
* Clear the cache
*/
async clear() {
this.cache = {
version: '1.0.0',
lastSync: null,
collections: {},
relations: [],
metadata: {}
};
await this.save();
console.log('🗑️ Cache cleared');
return true;
}
/**
* Export cache for debugging
*/
async export(outputFile = null) {
if (!this.cache) await this.load();
const exportFile = outputFile || path.join(this.cacheDir, `schema-export-${Date.now()}.json`);
const data = JSON.stringify(this.cache, null, 2);
await fs.writeFile(exportFile, data, 'utf-8');
console.log(`📤 Cache exported to: ${exportFile}`);
return exportFile;
}
}
// Singleton instance
let cacheInstance = null;
/**
* Get or create cache instance
*/
function getCache(cacheDir = null) {
if (!cacheInstance) {
cacheInstance = new SchemaCache(cacheDir);
}
return cacheInstance;
}
module.exports = {
SchemaCache,
getCache
};