/** * Schema Helper Functions * Reusable functions for creating Directus collections and fields * Now with integrated schema caching for offline tracking */ const { readCollections, createCollection: sdkCreateCollection, readFields, createField: sdkCreateField, createRelation: sdkCreateRelation } = require('@directus/sdk'); const { getCache } = require('./schema-cache'); /** * Create a collection with error handling * @param {Object} client - Directus client * @param {Object} collectionConfig - Collection configuration * @returns {Promise} Created collection or existing collection */ async function createCollection(client, collectionConfig) { const cache = getCache(); await cache.init(); try { // First check cache if (cache.hasCollection(collectionConfig.collection)) { console.log(`ℹ️ Collection '${collectionConfig.collection}' already exists (cached)`); return cache.getCollection(collectionConfig.collection); } // If not in cache, check Directus const existingCollections = await client.request(readCollections()); const exists = existingCollections.find(c => c.collection === collectionConfig.collection); if (exists) { console.log(`ℹ️ Collection '${collectionConfig.collection}' already exists`); // Update cache with existing collection await cache.addCollection(collectionConfig.collection, exists); return exists; } // Create collection const collection = await client.request( sdkCreateCollection(collectionConfig) ); console.log(`✅ Created collection: ${collectionConfig.collection}`); // Update cache with new collection await cache.addCollection(collectionConfig.collection, collection); return collection; } catch (error) { console.error(`❌ Failed to create collection ${collectionConfig.collection}:`, error.message); throw error; } } /** * Create a field with error handling * @param {Object} client - Directus client * @param {string} collection - Collection name * @param {Object} fieldConfig - Field configuration * @returns {Promise} Created field or existing field */ async function createField(client, collection, fieldConfig) { const cache = getCache(); await cache.init(); try { // First check cache if (cache.hasField(collection, fieldConfig.field)) { console.log(` ℹ️ Field '${fieldConfig.field}' already exists in '${collection}' (cached)`); return { ...cache.getField(collection, fieldConfig.field), alreadyExists: true }; } // If not in cache, check Directus const existingFields = await client.request(readFields(collection)); const exists = existingFields.find(f => f.field === fieldConfig.field); if (exists) { console.log(` ℹ️ Field '${fieldConfig.field}' already exists in '${collection}'`); // Update cache with existing field await cache.addField(collection, fieldConfig.field, exists); return { ...exists, alreadyExists: true }; } // Create field const field = await client.request( sdkCreateField(collection, fieldConfig) ); console.log(` ✅ Created field: ${fieldConfig.field}`); // Update cache with new field await cache.addField(collection, fieldConfig.field, field); return { ...field, created: true }; } catch (error) { // Provide more detailed error information const errorMessage = error.response?.data?.errors?.[0]?.message || error.message || 'Unknown error'; console.error(` ❌ Failed to create field ${fieldConfig.field} in ${collection}: ${errorMessage}`); // Return error info instead of throwing return { field: fieldConfig.field, error: true, errorMessage }; } } /** * Create multiple fields for a collection * @param {Object} client - Directus client * @param {string} collection - Collection name * @param {Array} fields - Array of field configurations * @returns {Promise} Created fields */ async function createFields(client, collection, fields) { const cache = getCache(); await cache.init(); const results = { created: [], existing: [], errors: [] }; // Process all fields without saving cache after each one for (const fieldConfig of fields) { try { // First check cache if (cache.hasField(collection, fieldConfig.field)) { results.existing.push(fieldConfig.field); continue; } // If not in cache, check Directus const existingFields = await client.request(readFields(collection)); const exists = existingFields.find(f => f.field === fieldConfig.field); if (exists) { // Add to cache without saving cache.cache.collections[collection] = cache.cache.collections[collection] || { fields: {} }; cache.cache.collections[collection].fields[fieldConfig.field] = exists; results.existing.push(fieldConfig.field); } else { // Create field try { const field = await client.request( sdkCreateField(collection, fieldConfig) ); console.log(` ✅ Created field: ${fieldConfig.field}`); // Add to cache without saving cache.cache.collections[collection] = cache.cache.collections[collection] || { fields: {} }; cache.cache.collections[collection].fields[fieldConfig.field] = field; results.created.push(fieldConfig.field); } catch (createError) { const errorMessage = createError.response?.data?.errors?.[0]?.message || createError.message || 'Unknown error'; console.error(` ❌ Failed to create field ${fieldConfig.field}: ${errorMessage}`); results.errors.push({ field: fieldConfig.field, message: errorMessage }); } } } catch (error) { results.errors.push({ field: fieldConfig.field, message: error.message }); } } // Save cache once after all fields processed if (results.created.length > 0 || results.existing.length > 0) { await cache.save(); } // Summary report if (results.created.length > 0) { console.log(` ✅ Created ${results.created.length} new fields`); } if (results.existing.length > 0) { console.log(` ℹ️ ${results.existing.length} fields already exist`); } if (results.errors.length > 0) { console.log(` ⚠️ ${results.errors.length} fields had errors:`); results.errors.forEach(err => { console.log(` - ${err.field}: ${err.message}`); }); } return results; } /** * Create a relationship between collections * @param {Object} client - Directus client * @param {Object} relationConfig - Relationship configuration * @returns {Promise} Created relationship */ async function createRelationship(client, relationConfig) { const cache = getCache(); await cache.init(); try { const relation = await client.request( sdkCreateRelation(relationConfig) ); console.log(`✅ Created relationship: ${relationConfig.collection} -> ${relationConfig.related_collection}`); // Update cache with new relation await cache.addRelation(relationConfig); return relation; } catch (error) { if (error.message.includes('already exists')) { console.log(`ℹ️ Relationship already exists: ${relationConfig.collection} -> ${relationConfig.related_collection}`); // Still update cache even if it exists await cache.addRelation(relationConfig); return null; } console.error(`❌ Failed to create relationship:`, error.message); throw error; } } /** * Standard field configurations */ const FIELD_TYPES = { uuid: (field, options = {}) => ({ field, type: 'uuid', schema: { is_primary_key: options.primary_key || false, has_auto_increment: false, is_nullable: options.nullable || false, default_value: options.default || null }, meta: { interface: 'input', special: options.special || null, options: options.options || null, display: options.display || null, readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'full', ...options.meta } }), string: (field, options = {}) => ({ field, type: 'string', schema: { is_nullable: options.nullable !== false, max_length: options.max_length || 255, default_value: options.default || null }, meta: { interface: options.interface || 'input', options: options.options || null, display: options.display || null, readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'full', required: options.required || false, ...options.meta } }), text: (field, options = {}) => ({ field, type: 'text', schema: { is_nullable: options.nullable !== false, default_value: options.default || null }, meta: { interface: options.interface || 'input-multiline', options: options.options || null, display: options.display || null, readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'full', ...options.meta } }), integer: (field, options = {}) => ({ field, type: 'integer', schema: { is_nullable: options.nullable !== false, default_value: options.default || null }, meta: { interface: options.interface || 'input', options: options.options || null, display: options.display || null, readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'half', ...options.meta } }), json: (field, options = {}) => ({ field, type: 'json', schema: { is_nullable: options.nullable !== false, default_value: options.default || null }, meta: { interface: options.interface || 'input-code', options: { language: 'json', ...options.options }, display: options.display || 'raw', readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'full', ...options.meta } }), datetime: (field, options = {}) => ({ field, type: 'timestamp', schema: { is_nullable: options.nullable !== false, default_value: options.default || null }, meta: { interface: options.interface || 'datetime', special: options.special || null, options: options.options || null, display: options.display || 'datetime', readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'half', ...options.meta } }), boolean: (field, options = {}) => ({ field, type: 'boolean', schema: { is_nullable: options.nullable || false, default_value: options.default !== undefined ? options.default : false }, meta: { interface: options.interface || 'boolean', special: options.special || null, options: options.options || null, display: options.display || 'boolean', readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'half', ...options.meta } }), decimal: (field, options = {}) => ({ field, type: 'decimal', schema: { is_nullable: options.nullable !== false, numeric_precision: options.precision || 10, numeric_scale: options.scale || 2, default_value: options.default || null }, meta: { interface: options.interface || 'input', options: options.options || null, display: options.display || null, readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'half', ...options.meta } }), date: (field, options = {}) => ({ field, type: 'date', schema: { is_nullable: options.nullable !== false, default_value: options.default || null }, meta: { interface: options.interface || 'datetime', special: options.special || null, options: options.options || null, display: options.display || 'datetime', readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'half', ...options.meta } }), dropdown: (field, choices, options = {}) => ({ field, type: 'string', schema: { is_nullable: options.nullable !== false, default_value: options.default || null, max_length: 255 }, meta: { interface: 'select-dropdown', options: { choices: choices.map(choice => typeof choice === 'string' ? { text: choice, value: choice } : choice ) }, display: options.display || null, readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'half', required: options.required || false, ...options.meta } }), m2o: (field, relatedCollection, options = {}) => ({ field, type: 'uuid', schema: { is_nullable: options.nullable !== false, foreign_key_table: relatedCollection, foreign_key_column: 'id' }, meta: { interface: 'select-dropdown-m2o', special: ['m2o'], options: { template: options.template || '{{id}}' }, display: options.display || null, readonly: options.readonly || false, hidden: options.hidden || false, width: options.width || 'full', ...options.meta } }) }; /** * Create system fields (created_at, updated_at, etc.) * @returns {Array} Array of system field configurations */ function getSystemFields() { return [ FIELD_TYPES.datetime('created_at', { special: ['date-created'], readonly: true, hidden: false }), FIELD_TYPES.datetime('updated_at', { special: ['date-updated'], readonly: true, hidden: false }) ]; } module.exports = { createCollection, createField, createFields, createRelationship, FIELD_TYPES, getSystemFields };