import { defineHook } from '@directus/extensions-sdk'; import schema from '../../schema.json'; export default defineHook(({ action, init }, { services, logger, database, getSchema }) => { logger.info('[inFrame Extension] ๐Ÿ”Œ Hook registered, waiting for events...'); // Hook on initialization event - runs after routes are registered init('routes.after', async () => { logger.info('[inFrame Extension] ๐Ÿš€ routes.after event triggered, running setup...'); try { await setupCollections({ services, logger, database, getSchema }); } catch (error: any) { logger.error(`[inFrame Extension] Error during initial setup: ${error.message}`); } }); // Hook on server start event (action) action('server.start', async () => { logger.info('[inFrame Extension] ๐Ÿš€ server.start event triggered, running setup...'); try { await setupCollections({ services, logger, database, getSchema }); } catch (error: any) { logger.error(`[inFrame Extension] Error during initial setup: ${error.message}`); } }); // Hook for when extension is installed/updated action('extensions.install', async ({ extension }: any) => { if (extension?.includes('inframe') || extension?.includes('@devix-tecnologia/directus-extension-inframe')) { logger.info('[inFrame Extension] Extension installed, configuring collections...'); try { await setupCollections({ services, logger, database, getSchema }); } catch (error: any) { logger.error(`[inFrame Extension] Error during installation: ${error.message}`); } } }); // Hook for when extensions are reloaded action('extensions.reload', async () => { logger.info('[inFrame Extension] Verifying collections configuration...'); try { await verifyCollections({ logger, services, getSchema }); } catch (error: any) { logger.warn(`[inFrame Extension] Error verifying collections: ${error.message}`); } }); }); // Function to verify if collections exist async function verifyCollections({ logger, services, getSchema }: any) { const { CollectionsService } = services; const currentSchema = await getSchema(); const collectionsService = new CollectionsService({ schema: currentSchema, knex: null as any, }); try { const allCollections = await collectionsService.readByQuery(); const existingCollectionNames = new Set(allCollections.map((c: any) => c.collection)); const ourCollections = schema.collections.map((c: any) => c.collection); const existingCount = ourCollections.filter((c) => existingCollectionNames.has(c)).length; const totalCount = ourCollections.length; if (existingCount === totalCount) { logger.info(`[inFrame Extension] All ${totalCount} collections are configured correctly โœ“`); } else { logger.warn(`[inFrame Extension] ${existingCount}/${totalCount} collections found. Run setup if needed.`); } } catch (error: any) { logger.warn(`[inFrame Extension] Error verifying collections: ${error.message}`); } } // Main function to create collections async function setupCollections({ services, logger, database, getSchema }: any) { const { CollectionsService, RelationsService, FieldsService } = services; logger.info('[inFrame Extension] Starting collections configuration...'); // Get current schema const currentSchema = await getSchema(); // Create collections service const collectionsService = new CollectionsService({ schema: currentSchema, knex: database, }); // Check if collections already exist using the service let allCollections: any[] = []; try { allCollections = await collectionsService.readByQuery(); } catch (error: any) { logger.warn(`[inFrame Extension] Error listing collections: ${error.message}`); allCollections = []; } const existingCollectionNames = new Set(allCollections.map((c: any) => c.collection)); let collectionsCreated = 0; let fieldsCreated = 0; let relationsCreated = 0; // Order collections by dependencies (folders first, then those that depend on them) const orderedCollections = [...schema.collections].sort((a: any, b: any) => { const aIsFolder = !a.meta?.group; // Collections without group (folders) come first const bIsFolder = !b.meta?.group; if (aIsFolder && !bIsFolder) return -1; if (!aIsFolder && bIsFolder) return 1; return 0; }); const collectionNames = orderedCollections.map((c: any) => c.collection); logger.info(`[inFrame Extension] ๐Ÿ“‹ Collections to be created (if they don't exist): ${collectionNames.join(', ')}`); // Create collections with included fields (as per official documentation) for (const collection of orderedCollections) { if (!existingCollectionNames.has(collection.collection)) { try { logger.info(`[inFrame Extension] ๐Ÿ”จ Creating collection: ${collection.collection}`); // Find fields that belong to this collection const collectionFields = schema.fields.filter((f: any) => f.collection === collection.collection); // Create only the collection first (WITHOUT fields) await collectionsService.createOne({ collection: collection.collection, meta: collection.meta, schema: collection.schema || null, }); collectionsCreated++; logger.info( `[inFrame Extension] โœ… Collection ${collection.collection} created (${collectionFields.length} field(s) will be created later)`, ); } catch (error: any) { logger.error(`[inFrame Extension] โŒ Error creating collection ${collection.collection}: ${error.message}`); } } else { logger.info(`[inFrame Extension] โญ๏ธ Collection ${collection.collection} already exists, checking fields...`); } } // If we created new collections, clear cache and force schema reload if (collectionsCreated > 0) { logger.info(`[inFrame Extension] ${collectionsCreated} collection(s) created`); logger.info('[inFrame Extension] ๐Ÿงน Waiting for schema propagation in Directus...'); // Wait for schema to be updated in database await new Promise((resolve) => setTimeout(resolve, 3000)); // Force schema update by calling getSchema multiple times for (let i = 0; i < 3; i++) { await getSchema({ accountability: null, database }); await new Promise((resolve) => setTimeout(resolve, 2000)); } logger.info('[inFrame Extension] โœ… Schema synchronized (waited 9 seconds)'); logger.info('[inFrame Extension] ๐Ÿ”จ Creating fields...'); // Create FieldsService with updated schema const updatedSchemaForFields = await getSchema({ accountability: null, database }); const fieldsService = new FieldsService({ schema: updatedSchemaForFields, knex: database, }); // Create fields for (const field of schema.fields) { try { // Check if field already exists const existingField = await database .select('*') .from('directus_fields') .where('collection', field.collection) .where('field', field.field) .first(); if (!existingField) { logger.info(`[inFrame Extension] ๐Ÿ”จ Creating field: ${field.collection}.${field.field}`); // For alias fields, we shouldn't pass the schema property const fieldData: any = { field: field.field, type: field.type, meta: field.meta, }; // Only add schema if not null (alias fields don't have schema) if (field.schema !== null) { fieldData.schema = field.schema; } await fieldsService.createField(field.collection, fieldData); fieldsCreated++; logger.info(`[inFrame Extension] โœ… Field ${field.collection}.${field.field} created`); } } catch (error: any) { logger.error( `[inFrame Extension] โŒ Error creating field ${field.collection}.${field.field}: ${error.message}`, ); } } logger.info(`[inFrame Extension] โœ… ${fieldsCreated} field(s) created`); } // Update schema for relations creation (force complete reload) const updatedSchema = await getSchema({ accountability: null, database }); logger.info('[inFrame Extension] ๐Ÿ“‹ Verifying relations...'); // Debug: list all collections available in schema const availableCollections = Object.keys(updatedSchema.collections || {}); const ourCollections = availableCollections.filter((c) => c.startsWith('inframe') || c === 'languages'); logger.info(`[inFrame Extension] ๐Ÿ” Collections found in schema: ${ourCollections.join(', ') || 'none'}`); logger.info(`[inFrame Extension] ๐Ÿ“Š Total collections in schema: ${availableCollections.length}`); // Recreate RelationsService with updated schema const updatedRelationsService = new RelationsService({ schema: updatedSchema, knex: database, }); // Create relations for (const relation of schema.relations) { try { // Check if involved collections exist directly in database // EXCEPT system collections (directus_*) if (!relation.collection.startsWith('directus_')) { const collectionExists = await database .select('collection') .from('directus_collections') .where('collection', relation.collection) .first(); if (!collectionExists) { logger.warn( `[inFrame Extension] โš ๏ธ Collection ${relation.collection} not found in database, skipping relation`, ); continue; } } if (relation.related_collection && !relation.related_collection.startsWith('directus_')) { const relatedExists = await database .select('collection') .from('directus_collections') .where('collection', relation.related_collection) .first(); if (!relatedExists) { logger.warn( `[inFrame Extension] โš ๏ธ Related collection ${relation.related_collection} not found in database, skipping relation`, ); continue; } } // Check if relation already exists const existingRelationCheck = await database .select('*') .from('directus_relations') .where('many_collection', relation.collection) .where('many_field', relation.field) .first(); if (existingRelationCheck) { logger.info(`[inFrame Extension] โญ๏ธ Relation ${relation.collection}.${relation.field} already exists`); continue; } logger.info(`[inFrame Extension] ๐Ÿ”— Creating relation: ${relation.collection}.${relation.field}`); // Since schema is not updated in time, let's create the relation directly in database try { await database('directus_relations').insert({ many_collection: relation.meta.many_collection, many_field: relation.meta.many_field, one_collection: relation.meta.one_collection, one_field: relation.meta.one_field, one_collection_field: relation.meta.one_collection_field, one_allowed_collections: relation.meta.one_allowed_collections ? JSON.stringify(relation.meta.one_allowed_collections) : null, junction_field: relation.meta.junction_field, sort_field: relation.meta.sort_field, one_deselect_action: relation.meta.one_deselect_action || 'nullify', }); relationsCreated++; logger.info( `[inFrame Extension] โœ… Relation ${relation.collection}.${relation.field} created directly in database`, ); } catch (dbError: any) { // If error occurs (e.g.: already exists), try with service logger.warn(`[inFrame Extension] Direct attempt failed: ${dbError.message}, trying with service...`); await updatedRelationsService.createOne({ collection: relation.collection, field: relation.field, related_collection: relation.related_collection, meta: relation.meta, schema: relation.schema, }); relationsCreated++; logger.info(`[inFrame Extension] โœ… Relation ${relation.collection}.${relation.field} created via service`); } } catch (error: any) { logger.error( `[inFrame Extension] โŒ Error creating relation ${relation.collection}.${relation.field}: ${error.message}`, ); } } logger.info( `[inFrame Extension] โœ… Configuration complete! Created: ${collectionsCreated} collection(s), ${fieldsCreated} field(s), ${relationsCreated} relation(s)`, ); // Force schema update and clear caches try { logger.info('[inFrame Extension] ๐Ÿ”„ Clearing cache and updating schema...'); // Force a new schema read await getSchema({ accountability: null, database }); // Wait a bit to ensure schema was updated await new Promise((resolve) => setTimeout(resolve, 2000)); logger.info('[inFrame Extension] โœ… Cache cleared and schema updated'); } catch (cacheError: any) { logger.warn(`[inFrame Extension] โš ๏ธ Error clearing cache: ${cacheError.message}`); } }