Skip to content

Commit f3fd98e

Browse files
author
Lasim
committed
feat: implement plugin migration functionality and update createPluginTables logic
1 parent f2cda43 commit f3fd98e

File tree

4 files changed

+501
-233
lines changed

4 files changed

+501
-233
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/backend/src/db/index.ts

Lines changed: 6 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,9 @@ export function executeDbOperation<T>(
516516
return operation(db, schema);
517517
}
518518

519+
// Import plugin migration functionality
520+
import { createPluginTables as createPluginTablesImpl } from './plugin-migrations';
521+
519522
// Plugin system functions
520523
interface DatabaseExtensionWithTables extends DatabaseExtension {
521524

@@ -543,240 +546,15 @@ export function registerPluginTables(plugins: Plugin[], logger?: FastifyBaseLogg
543546
}
544547

545548
export async function createPluginTables(plugins: Plugin[], logger: FastifyBaseLogger) {
546-
// Plugin tables are now handled by migrations
547-
logger.info({
548-
operation: 'create_plugin_tables'
549-
}, 'Plugin tables are handled by migrations.');
550-
return;
551-
552-
if (!dbInstance || !isDbInitialized) {
549+
if (!dbInstance || !isDbInitialized || !dbConfig) {
553550
logger.warn({
554551
operation: 'create_plugin_tables'
555552
}, 'Database not initialized, skipping plugin table creation.');
556553
return;
557554
}
558555

559-
const pluginsWithTables = plugins.filter(plugin =>
560-
plugin.databaseExtension && plugin.databaseExtension.tableDefinitions
561-
);
562-
563-
if (pluginsWithTables.length === 0) {
564-
logger.info({
565-
operation: 'create_plugin_tables'
566-
}, 'No plugins with table definitions found.');
567-
return;
568-
}
569-
570-
logger.info({
571-
operation: 'create_plugin_tables',
572-
pluginCount: pluginsWithTables.length
573-
}, `Creating tables for ${pluginsWithTables.length} plugins...`);
574-
575-
for (const plugin of pluginsWithTables) {
576-
const ext = plugin.databaseExtension as DatabaseExtensionWithTables;
577-
if (!ext.tableDefinitions) continue;
578-
579-
for (const [tableName, columnDefs] of Object.entries(ext.tableDefinitions || {})) {
580-
const fullTableName = `${plugin.meta.id}_${tableName}`;
581-
582-
try {
583-
// Generate CREATE TABLE SQL dynamically
584-
const createTableSQL = generateCreateTableSQL(fullTableName, columnDefs);
585-
586-
logger.debug({
587-
operation: 'create_plugin_tables',
588-
pluginId: plugin.meta.id,
589-
tableName: fullTableName,
590-
sql: createTableSQL
591-
}, `Creating plugin table: ${fullTableName}`);
592-
593-
// Drop the table first to ensure clean recreation (for development)
594-
const dropTableSQL = `DROP TABLE IF EXISTS "${fullTableName}"`;
595-
596-
logger.debug({
597-
operation: 'create_plugin_tables',
598-
pluginId: plugin.meta.id,
599-
tableName: fullTableName,
600-
sql: dropTableSQL
601-
}, `Dropping existing plugin table: ${fullTableName}`);
602-
603-
// Execute the DROP TABLE statement
604-
if (dbConfig?.type === 'sqlite') {
605-
(dbInstance as any).$client.exec(dropTableSQL);
606-
} else if (dbConfig?.type === 'turso') {
607-
if ((dbInstance as any).$client && typeof (dbInstance as any).$client.execute === 'function') {
608-
await (dbInstance as any).$client.execute(dropTableSQL);
609-
} else {
610-
await dbInstance.run(dropTableSQL);
611-
}
612-
}
613-
614-
// Execute the CREATE TABLE statement
615-
if (dbConfig?.type === 'sqlite') {
616-
(dbInstance as any).$client.exec(createTableSQL);
617-
} else if (dbConfig?.type === 'turso') {
618-
if ((dbInstance as any).$client && typeof (dbInstance as any).$client.execute === 'function') {
619-
await (dbInstance as any).$client.execute(createTableSQL);
620-
} else {
621-
await dbInstance.run(createTableSQL);
622-
}
623-
}
624-
625-
logger.info({
626-
operation: 'create_plugin_tables',
627-
pluginId: plugin.meta.id,
628-
tableName: fullTableName
629-
}, `✅ Created plugin table: ${fullTableName}`);
630-
631-
} catch (error) {
632-
const typedError = error as Error;
633-
// Check if table already exists (not an error)
634-
if (typedError.message.includes('already exists') || typedError.message.includes('table') && typedError.message.includes('already')) {
635-
logger.debug({
636-
operation: 'create_plugin_tables',
637-
pluginId: plugin.meta.id,
638-
tableName: fullTableName
639-
}, `Table ${fullTableName} already exists, skipping.`);
640-
} else {
641-
logger.error({
642-
operation: 'create_plugin_tables',
643-
pluginId: plugin.meta.id,
644-
tableName: fullTableName,
645-
error: typedError,
646-
message: typedError.message
647-
}, `❌ Failed to create plugin table: ${fullTableName}`);
648-
throw error;
649-
}
650-
}
651-
}
652-
}
653-
654-
logger.info({
655-
operation: 'create_plugin_tables'
656-
}, '✅ Plugin table creation completed.');
657-
}
658-
659-
/**
660-
* Generate CREATE TABLE SQL from plugin table definitions
661-
*/
662-
function generateCreateTableSQL(tableName: string, columnDefs: Record<string, (columnBuilder: any) => any>): string {
663-
const columns: string[] = [];
664-
665-
for (const [columnName, columnDefFunc] of Object.entries(columnDefs)) {
666-
// Create a mock column builder to extract column definition
667-
const mockBuilder = createMockColumnBuilder();
668-
const columnDef = columnDefFunc(mockBuilder);
669-
670-
// Convert the column definition to SQL
671-
const sqlColumn = convertColumnDefToSQL(columnName, columnDef);
672-
columns.push(sqlColumn);
673-
}
674-
675-
return `CREATE TABLE IF NOT EXISTS "${tableName}" (\n ${columns.join(',\n ')}\n)`;
676-
}
677-
678-
/**
679-
* Create a mock column builder that captures column definition details
680-
*/
681-
function createMockColumnBuilder() {
682-
const createColumn = (type: string) => {
683-
const column = {
684-
type,
685-
isPrimaryKey: false,
686-
isNotNull: false,
687-
isUnique: false,
688-
defaultValue: undefined as any,
689-
references: undefined as any,
690-
};
691-
692-
return {
693-
...column,
694-
primaryKey() {
695-
column.isPrimaryKey = true;
696-
return this;
697-
},
698-
699-
notNull() {
700-
column.isNotNull = true;
701-
return this;
702-
},
703-
704-
unique() {
705-
column.isUnique = true;
706-
return this;
707-
},
708-
709-
default(value: any) {
710-
column.defaultValue = value;
711-
return this;
712-
},
713-
714-
defaultNow() {
715-
column.defaultValue = "strftime('%s', 'now')";
716-
return this;
717-
},
718-
719-
references(ref: any) {
720-
column.references = ref;
721-
return this;
722-
}
723-
};
724-
};
725-
726-
return (columnName: string, options?: any) => {
727-
// Determine column type based on name patterns and options
728-
let type = 'TEXT';
729-
730-
if (options?.mode === 'timestamp' || columnName.toLowerCase().includes('at') || columnName.toLowerCase().includes('date')) {
731-
type = 'INTEGER'; // SQLite uses INTEGER for timestamps
732-
} else if (columnName.toLowerCase().includes('id') || columnName.toLowerCase().includes('count') ||
733-
columnName.toLowerCase().includes('age') || columnName.toLowerCase().includes('quantity') ||
734-
columnName.toLowerCase().includes('order') || columnName.toLowerCase().includes('number')) {
735-
type = 'INTEGER';
736-
}
737-
738-
const column = createColumn(type);
739-
740-
// For timestamp columns with mode, automatically set default if not already set
741-
if (options?.mode === 'timestamp' && !column.defaultValue) {
742-
column.defaultValue = "strftime('%s', 'now')";
743-
}
744-
745-
return column;
746-
};
747-
}
748-
749-
/**
750-
* Convert column definition object to SQL string
751-
*/
752-
function convertColumnDefToSQL(columnName: string, columnDef: any): string {
753-
let sql = `"${columnName}" ${columnDef.type}`;
754-
755-
if (columnDef.isPrimaryKey) {
756-
sql += ' PRIMARY KEY';
757-
}
758-
759-
if (columnDef.isNotNull) {
760-
sql += ' NOT NULL';
761-
}
762-
763-
if (columnDef.isUnique) {
764-
sql += ' UNIQUE';
765-
}
766-
767-
if (columnDef.defaultValue !== undefined) {
768-
if (typeof columnDef.defaultValue === 'string' && columnDef.defaultValue.includes('strftime')) {
769-
sql += ` DEFAULT (${columnDef.defaultValue})`;
770-
} else if (typeof columnDef.defaultValue === 'string') {
771-
sql += ` DEFAULT '${columnDef.defaultValue}'`;
772-
} else if (typeof columnDef.defaultValue === 'boolean') {
773-
sql += ` DEFAULT ${columnDef.defaultValue ? 1 : 0}`;
774-
} else {
775-
sql += ` DEFAULT ${columnDef.defaultValue}`;
776-
}
777-
}
778-
779-
return sql;
556+
// Use the extracted plugin migration functionality
557+
await createPluginTablesImpl(plugins, dbInstance, dbConfig, logger);
780558
}
781559

782560
export async function initializePluginDatabases(db: AnyDatabase, plugins: Plugin[], logger: FastifyBaseLogger) {

0 commit comments

Comments
 (0)