directus-task-management/scripts/utils/schema-helpers.js

494 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<Object>} 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<Object>} 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<Array>} 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<Object>} 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
};