#!/usr/bin/env node
/**
* Schema Upload Script for Directus
*
* Usage: node scripts/upload-schema.js <schema-file.json>
* Example: node scripts/upload-schema.js schemas/tasks.json
*/
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load environment variables
dotenv.config({ path: path.join(__dirname, '..', '.env') });
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://te9-pg-api.up.railway.app/';
const DIRECTUS_TOKEN = process.env.DIRECTUS_TOKEN;
if (!DIRECTUS_TOKEN) {
console.error('ā DIRECTUS_TOKEN environment variable is required');
process.exit(1);
}
/**
* Create Directus API client
*/
function createDirectusClient() {
return axios.create({
baseURL: DIRECTUS_URL.replace(/\/$/, ''),
headers: {
'Authorization': `Bearer ${DIRECTUS_TOKEN}`,
'Content-Type': 'application/json'
}
});
}
/**
* Check if collection exists
*/
async function collectionExists(client, collectionName) {
try {
console.log(`š Checking if collection "${collectionName}" exists...`);
await client.get(`/collections/${collectionName}`);
console.log(`ā
Collection "${collectionName}" already exists`);
return true;
} catch (error) {
if (error.response?.status === 404 || error.response?.status === 403) {
console.log(`š Collection "${collectionName}" does not exist - will create`);
return false;
}
console.error(`ā Error checking collection: ${error.message}`);
throw error;
}
}
/**
* Create collection
*/
async function createCollection(client, schema) {
const collectionData = {
collection: schema.name,
meta: {
collection: schema.name,
note: schema.comment || `Auto-imported schema for ${schema.name}`,
hidden: false,
singleton: false
},
schema: {
name: schema.name
}
};
console.log(`š¦ Creating collection: ${schema.name}`);
try {
await client.post('/collections', collectionData);
console.log(`ā
Collection created successfully`);
} catch (error) {
console.error(`ā Error creating collection: ${error.message}`);
if (error.response?.data) {
console.error(`š Response:`, JSON.stringify(error.response.data, null, 2));
}
throw error;
}
}
/**
* Create field
*/
async function createField(client, collectionName, field) {
const fieldData = {
field: field.field,
type: mapFieldType(field.type),
meta: {
collection: collectionName,
field: field.field,
special: getSpecialFlags(field),
interface: getFieldInterface(field),
options: getFieldOptions(field),
display: 'raw',
readonly: field.readonly || false,
hidden: field.hidden || false,
required: field.required || false,
note: field.comment || null
},
schema: {
name: field.field,
table: collectionName,
data_type: getDataType(field.type),
default_value: field.default_value || null,
max_length: field.length || null,
is_nullable: field.nullable !== false && !field.required,
is_unique: field.unique || false,
is_primary_key: field.primary || false,
has_auto_increment: field.auto_increment || false
}
};
console.log(` š Creating field: ${field.field} (${field.type})`);
try {
await client.post(`/fields/${collectionName}`, fieldData);
console.log(` ā
Field created: ${field.field}`);
} catch (error) {
console.error(` ā Field creation failed: ${field.field} - ${error.response?.data?.errors?.[0]?.message || error.message}`);
// Continue with other fields for non-critical errors
if (error.response?.status !== 400) {
throw error;
}
}
}
/**
* Map field type to Directus type
*/
function mapFieldType(type) {
switch (type) {
case 'uuid': return 'uuid';
case 'string': return 'string';
case 'text': return 'text';
case 'integer': return 'integer';
case 'decimal': return 'decimal';
case 'float': return 'float';
case 'boolean': return 'boolean';
case 'date': return 'date';
case 'timestamp': return 'timestamp';
case 'json': return 'json';
default: return 'string';
}
}
/**
* Get special flags for field
*/
function getSpecialFlags(field) {
const special = [];
if (field.primary) special.push('uuid');
if (field.field === 'created_at') special.push('date-created');
if (field.field === 'updated_at') special.push('date-updated');
return special.length > 0 ? special : null;
}
/**
* Get field interface
*/
function getFieldInterface(field) {
if (field.primary) return 'input';
if (field.enum) return 'select-dropdown';
if (field.field === 'created_at' || field.field === 'updated_at') return 'datetime';
switch (field.type) {
case 'uuid': return 'input';
case 'string': return 'input';
case 'text': return 'input-multiline';
case 'integer': return 'input';
case 'decimal': return 'input';
case 'float': return 'input';
case 'boolean': return 'boolean';
case 'date': return 'datetime';
case 'timestamp': return 'datetime';
case 'json': return 'tags';
default: return 'input';
}
}
/**
* Get field options
*/
function getFieldOptions(field) {
const options = {};
if (field.enum && Array.isArray(field.enum)) {
options.choices = field.enum.map(value => ({
text: value.charAt(0).toUpperCase() + value.slice(1).replace('_', ' '),
value: value
}));
}
if (field.length) {
options.maxLength = field.length;
}
return Object.keys(options).length > 0 ? options : null;
}
/**
* Get database data type
*/
function getDataType(fieldType) {
switch (fieldType) {
case 'uuid': return 'uuid';
case 'string': return 'varchar';
case 'text': return 'text';
case 'integer': return 'integer';
case 'decimal': return 'decimal';
case 'float': return 'real';
case 'boolean': return 'boolean';
case 'date': return 'date';
case 'timestamp': return 'timestamp';
case 'json': return 'json';
default: return 'varchar';
}
}
/**
* Upload schema
*/
async function uploadSchema(schemaFilePath) {
try {
console.log(`š Starting schema upload: ${schemaFilePath}`);
console.log(`š Directus URL: ${DIRECTUS_URL}`);
console.log(`š Using token: ***${DIRECTUS_TOKEN.slice(-4)}`);
// Read schema file
const absolutePath = path.resolve(schemaFilePath);
if (!fs.existsSync(absolutePath)) {
throw new Error(`Schema file not found: ${absolutePath}`);
}
const schemaContent = fs.readFileSync(absolutePath, 'utf8');
const schema = JSON.parse(schemaContent);
console.log(`š Schema loaded: ${schema.name}`);
console.log(`š Fields: ${schema.fields?.length || 0}`);
if (!schema.name || !schema.fields) {
throw new Error('Invalid schema format. Must have "name" and "fields" properties.');
}
// Create client
const client = createDirectusClient();
// Test connection
try {
await client.get('/server/info');
console.log(`ā
Connected to Directus successfully`);
} catch (error) {
throw new Error(`Failed to connect to Directus: ${error.message}`);
}
// Check if collection exists
const exists = await collectionExists(client, schema.name);
if (exists) {
console.log(`ā ļø Collection "${schema.name}" already exists. Skipping creation.`);
} else {
await createCollection(client, schema);
}
// Create fields
console.log(`š Creating ${schema.fields.length} fields...`);
for (const field of schema.fields) {
await createField(client, schema.name, field);
}
console.log(`\nš Schema upload completed successfully!`);
console.log(`š Collection: ${schema.name}`);
console.log(`š Fields processed: ${schema.fields.length}`);
console.log(`š Access at: ${DIRECTUS_URL}/admin/content/${schema.name}`);
} catch (error) {
console.error(`ā Schema upload failed: ${error.message}`);
if (error.response?.data) {
console.error(`š Server response:`, JSON.stringify(error.response.data, null, 2));
}
process.exit(1);
}
}
// Main
const args = process.argv.slice(2);
if (args.length === 0) {
console.log(`
š Schema Upload Script for Directus
Usage:
node scripts/upload-schema.js <schema-file.json>
Example:
node scripts/upload-schema.js schemas/tasks.json
`);
process.exit(1);
}
const schemaFile = args[0];
uploadSchema(schemaFile);