471 lines
12 KiB
JavaScript
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
|
|
}; |