#!/usr/bin/env node /** * Directus Schema Setup via REST API * * Erstellt automatisch alle benΓΆtigten Collections, Fields und Relations * fΓΌr Tech Stack in Directus via REST API. * * Usage: * npm install node-fetch@2 * node scripts/setup-directus-collections.js */ const fetch = require('node-fetch'); require('dotenv').config(); const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; if (!DIRECTUS_TOKEN) { console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env'); process.exit(1); } console.log(`πŸ”— Connecting to: ${DIRECTUS_URL}`); async function directusRequest(endpoint, method = 'GET', body = null) { const url = `${DIRECTUS_URL}/${endpoint}`; const options = { method, headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } }; if (body) { options.body = JSON.stringify(body); } try { const response = await fetch(url, options); const text = await response.text(); if (!response.ok) { // Ignore "already exists" errors if (text.includes('already exists') || text.includes('RECORD_NOT_UNIQUE')) { console.log(` ⚠️ Already exists, skipping...`); return { data: null, alreadyExists: true }; } throw new Error(`HTTP ${response.status}: ${text}`); } return text ? JSON.parse(text) : {}; } catch (error) { console.error(`❌ Error calling ${method} ${endpoint}:`, error.message); throw error; } } async function ensureLanguages() { console.log('\n🌍 Setting up Languages...'); try { // Check if languages collection exists const { data: existing } = await directusRequest('items/languages'); if (!existing) { console.log(' Creating languages collection...'); await directusRequest('collections', 'POST', { collection: 'languages', meta: { icon: 'translate', translations: [ { language: 'en-US', translation: 'Languages' } ] }, schema: { name: 'languages' } }); } // Add en-US await directusRequest('items/languages', 'POST', { code: 'en-US', name: 'English (United States)' }); // Add de-DE await directusRequest('items/languages', 'POST', { code: 'de-DE', name: 'German (Germany)' }); console.log(' βœ… Languages ready (en-US, de-DE)'); } catch (error) { console.log(' ⚠️ Languages might already exist'); } } async function createTechStackCollections() { console.log('\nπŸ“¦ Creating Tech Stack Collections...\n'); // 1. Create tech_stack_categories collection console.log('1️⃣ Creating tech_stack_categories...'); try { await directusRequest('collections', 'POST', { collection: 'tech_stack_categories', meta: { icon: 'layers', display_template: '{{translations.name}}', hidden: false, singleton: false, translations: [ { language: 'en-US', translation: 'Tech Stack Categories' }, { language: 'de-DE', translation: 'Tech Stack Kategorien' } ], sort_field: 'sort' }, schema: { name: 'tech_stack_categories' } }); console.log(' βœ… Collection created'); } catch (error) { console.log(' ⚠️ Collection might already exist'); } // 2. Create tech_stack_categories_translations collection console.log('\n2️⃣ Creating tech_stack_categories_translations...'); try { await directusRequest('collections', 'POST', { collection: 'tech_stack_categories_translations', meta: { hidden: true, icon: 'import_export' }, schema: { name: 'tech_stack_categories_translations' } }); console.log(' βœ… Collection created'); } catch (error) { console.log(' ⚠️ Collection might already exist'); } // 3. Create tech_stack_items collection console.log('\n3️⃣ Creating tech_stack_items...'); try { await directusRequest('collections', 'POST', { collection: 'tech_stack_items', meta: { icon: 'code', display_template: '{{name}}', hidden: false, singleton: false, sort_field: 'sort' }, schema: { name: 'tech_stack_items' } }); console.log(' βœ… Collection created'); } catch (error) { console.log(' ⚠️ Collection might already exist'); } } async function createFields() { console.log('\nπŸ”§ Creating Fields...\n'); // Fields for tech_stack_categories console.log('1️⃣ Fields for tech_stack_categories:'); const categoryFields = [ { field: 'status', type: 'string', meta: { interface: 'select-dropdown', options: { choices: [ { text: 'Published', value: 'published' }, { text: 'Draft', value: 'draft' }, { text: 'Archived', value: 'archived' } ] } }, schema: { default_value: 'draft', is_nullable: false } }, { field: 'sort', type: 'integer', meta: { interface: 'input', hidden: true }, schema: {} }, { field: 'key', type: 'string', meta: { interface: 'input', note: 'Unique identifier (e.g. frontend, backend)' }, schema: { is_unique: true, is_nullable: false } }, { field: 'icon', type: 'string', meta: { interface: 'select-dropdown', options: { choices: [ { text: 'Globe', value: 'Globe' }, { text: 'Server', value: 'Server' }, { text: 'Wrench', value: 'Wrench' }, { text: 'Shield', value: 'Shield' } ] } }, schema: { default_value: 'Code' } }, { field: 'date_created', type: 'timestamp', meta: { special: ['date-created'], interface: 'datetime', readonly: true, hidden: true }, schema: {} }, { field: 'date_updated', type: 'timestamp', meta: { special: ['date-updated'], interface: 'datetime', readonly: true, hidden: true }, schema: {} }, { field: 'translations', type: 'alias', meta: { special: ['translations'], interface: 'translations', options: { languageField: 'languages_code' } } } ]; for (const field of categoryFields) { try { await directusRequest('fields/tech_stack_categories', 'POST', field); console.log(` βœ… ${field.field}`); } catch (error) { console.log(` ⚠️ ${field.field} (might already exist)`); } } // Fields for tech_stack_categories_translations console.log('\n2️⃣ Fields for tech_stack_categories_translations:'); const translationFields = [ { field: 'tech_stack_categories_id', type: 'uuid', meta: { hidden: true }, schema: {} }, { field: 'languages_code', type: 'string', meta: { interface: 'select-dropdown-m2o' }, schema: {} }, { field: 'name', type: 'string', meta: { interface: 'input', note: 'Translated category name' }, schema: {} } ]; for (const field of translationFields) { try { await directusRequest('fields/tech_stack_categories_translations', 'POST', field); console.log(` βœ… ${field.field}`); } catch (error) { console.log(` ⚠️ ${field.field} (might already exist)`); } } // Fields for tech_stack_items console.log('\n3️⃣ Fields for tech_stack_items:'); const itemFields = [ { field: 'sort', type: 'integer', meta: { interface: 'input', hidden: true }, schema: {} }, { field: 'category', type: 'uuid', meta: { interface: 'select-dropdown-m2o' }, schema: {} }, { field: 'name', type: 'string', meta: { interface: 'input', note: 'Technology name (e.g. Next.js, Docker)' }, schema: { is_nullable: false } }, { field: 'url', type: 'string', meta: { interface: 'input', note: 'Official website (optional)' }, schema: {} }, { field: 'icon_url', type: 'string', meta: { interface: 'input', note: 'Custom icon URL (optional)' }, schema: {} }, { field: 'date_created', type: 'timestamp', meta: { special: ['date-created'], interface: 'datetime', readonly: true, hidden: true }, schema: {} }, { field: 'date_updated', type: 'timestamp', meta: { special: ['date-updated'], interface: 'datetime', readonly: true, hidden: true }, schema: {} } ]; for (const field of itemFields) { try { await directusRequest('fields/tech_stack_items', 'POST', field); console.log(` βœ… ${field.field}`); } catch (error) { console.log(` ⚠️ ${field.field} (might already exist)`); } } } async function createRelations() { console.log('\nπŸ”— Creating Relations...\n'); const relations = [ { collection: 'tech_stack_categories_translations', field: 'tech_stack_categories_id', related_collection: 'tech_stack_categories', meta: { one_field: 'translations', sort_field: null, one_deselect_action: 'delete' }, schema: { on_delete: 'CASCADE' } }, { collection: 'tech_stack_categories_translations', field: 'languages_code', related_collection: 'languages', meta: { one_field: null, sort_field: null, one_deselect_action: 'nullify' }, schema: { on_delete: 'SET NULL' } }, { collection: 'tech_stack_items', field: 'category', related_collection: 'tech_stack_categories', meta: { one_field: 'items', sort_field: 'sort', one_deselect_action: 'nullify' }, schema: { on_delete: 'SET NULL' } } ]; for (let i = 0; i < relations.length; i++) { try { await directusRequest('relations', 'POST', relations[i]); console.log(` βœ… Relation ${i + 1}/${relations.length}`); } catch (error) { console.log(` ⚠️ Relation ${i + 1}/${relations.length} (might already exist)`); } } } async function main() { console.log('\n╔════════════════════════════════════════╗'); console.log('β•‘ Directus Tech Stack Setup via API β•‘'); console.log('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n'); try { await ensureLanguages(); await createTechStackCollections(); await createFields(); await createRelations(); console.log('\n╔════════════════════════════════════════╗'); console.log('β•‘ βœ… Setup Complete! β•‘'); console.log('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n'); console.log('πŸŽ‰ Tech Stack Collections sind bereit!\n'); console.log('NΓ€chste Schritte:'); console.log(' 1. Besuche: https://cms.dk0.dev/admin/content/tech_stack_categories'); console.log(' 2. FΓΌhre aus: node scripts/migrate-tech-stack-to-directus.js'); console.log(' 3. Verifiziere die Daten im Directus Admin Panel\n'); } catch (error) { console.error('\n❌ Setup failed:', error); console.error('\nTroubleshooting:'); console.error(' - ÜberprΓΌfe DIRECTUS_URL und DIRECTUS_STATIC_TOKEN in .env'); console.error(' - Stelle sicher, dass der Token Admin-Rechte hat'); console.error(' - PrΓΌfe ob Directus erreichbar ist: curl ' + DIRECTUS_URL); process.exit(1); } } main();