494 lines
14 KiB
JavaScript
494 lines
14 KiB
JavaScript
/**
|
||
* 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
|
||
}; |