@invect/core 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/database/core-schema.cjs.map +1 -1
- package/dist/database/core-schema.js.map +1 -1
- package/dist/database/prisma-schema-generator.cjs +2 -2
- package/dist/database/prisma-schema-generator.cjs.map +1 -1
- package/dist/database/prisma-schema-generator.js +2 -2
- package/dist/database/prisma-schema-generator.js.map +1 -1
- package/dist/database/schema-generator.cjs +6 -6
- package/dist/database/schema-generator.cjs.map +1 -1
- package/dist/database/schema-generator.js +6 -6
- package/dist/database/schema-generator.js.map +1 -1
- package/dist/database/schema-merger.cjs.map +1 -1
- package/dist/database/schema-merger.js.map +1 -1
- package/dist/database/schema-verification.cjs +1 -1
- package/dist/database/schema-verification.cjs.map +1 -1
- package/dist/database/schema-verification.js +1 -1
- package/dist/database/schema-verification.js.map +1 -1
- package/dist/services/database/database.service.cjs +3 -3
- package/dist/services/database/database.service.cjs.map +1 -1
- package/dist/services/database/database.service.js +3 -3
- package/dist/services/database/database.service.js.map +1 -1
- package/dist/types/plugin.types.d.cts +1 -1
- package/dist/types/plugin.types.d.ts +1 -1
- package/dist/types/schemas-fresh/invect-config.cjs.map +1 -1
- package/dist/types/schemas-fresh/invect-config.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-merger.js","names":[],"sources":["../../src/database/schema-merger.ts"],"sourcesContent":["/**\n * Schema Merger\n *\n * Merges the core abstract schema with plugin schemas to produce a\n * unified abstract schema. The CLI schema generator then converts\n * this merged schema into dialect-specific Drizzle files.\n *\n * Handles:\n * - New plugin tables\n * - Plugin fields added to existing core tables (additive only)\n * - Duplicate field detection (throws error)\n * - Ordering by foreign key dependencies\n */\n\nimport type { InvectPlugin, PluginTableDefinition } from 'src/types/plugin.types';\nimport { CORE_SCHEMA, CORE_TABLE_NAMES } from './core-schema';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface MergedSchema {\n /** All tables (core + plugin) in dependency-resolved order */\n tables: MergedTable[];\n /** Which plugin contributed each table/field (for diagnostics) */\n provenance: SchemaProvenance[];\n}\n\nexport interface MergedTable {\n /** Logical table name (camelCase) */\n name: string;\n /** The merged table definition */\n definition: PluginTableDefinition;\n /** Which source this table came from */\n source: 'core' | string; // 'core' or plugin ID\n}\n\nexport interface SchemaProvenance {\n /** Table name */\n table: string;\n /** Field name (null for table-level provenance) */\n field: string | null;\n /** Source: 'core' or plugin ID */\n source: string;\n}\n\nexport interface SchemaMergeError {\n type: 'duplicate_field' | 'duplicate_table' | 'invalid_reference';\n message: string;\n table: string;\n field?: string;\n plugin: string;\n}\n\n// =============================================================================\n// Merge Function\n// =============================================================================\n\n/**\n * Merge core schema with all plugin schemas.\n *\n * @param plugins - Array of plugins (only those with `schema` are processed)\n * @returns Merged schema with tables in dependency order\n * @throws Error if there are conflicting field definitions\n */\nexport function mergeSchemas(plugins: InvectPlugin[]): MergedSchema {\n const errors: SchemaMergeError[] = [];\n const provenance: SchemaProvenance[] = [];\n\n // Start with a deep copy of the core schema\n const merged: Record<string, PluginTableDefinition> = {};\n const tableSources: Record<string, string> = {}; // table → source\n\n // Add core tables\n for (const [name, def] of Object.entries(CORE_SCHEMA)) {\n merged[name] = deepCopyTableDef(def);\n tableSources[name] = 'core';\n provenance.push({ table: name, field: null, source: 'core' });\n for (const fieldName of Object.keys(def.fields)) {\n provenance.push({ table: name, field: fieldName, source: 'core' });\n }\n }\n\n // Merge each plugin's schema\n for (const plugin of plugins) {\n if (!plugin.schema) {\n continue;\n }\n\n for (const [tableName, tableDef] of Object.entries(plugin.schema)) {\n if (tableDef.disableMigration) {\n continue;\n }\n\n const _isCoreTable = CORE_TABLE_NAMES.includes(tableName);\n const existsAlready = tableName in merged;\n\n if (existsAlready) {\n // Extending an existing table — merge fields additively\n const existing = merged[tableName];\n if (!existing) {\n continue;\n }\n\n for (const [fieldName, fieldDef] of Object.entries(tableDef.fields)) {\n if (fieldName in existing.fields) {\n // Field already exists — error unless it's from the same source\n const existingSource = provenance.find(\n (p) => p.table === tableName && p.field === fieldName,\n )?.source;\n\n if (existingSource && existingSource !== plugin.id) {\n errors.push({\n type: 'duplicate_field',\n message: `Field \"${fieldName}\" on table \"${tableName}\" already defined by \"${existingSource}\". Plugin \"${plugin.id}\" cannot override it.`,\n table: tableName,\n field: fieldName,\n plugin: plugin.id,\n });\n }\n continue; // Skip duplicate\n }\n\n // Add new field to existing table\n existing.fields[fieldName] = { ...fieldDef };\n provenance.push({ table: tableName, field: fieldName, source: plugin.id });\n }\n } else {\n // New table from plugin\n merged[tableName] = deepCopyTableDef(tableDef);\n tableSources[tableName] = plugin.id;\n provenance.push({ table: tableName, field: null, source: plugin.id });\n\n for (const fieldName of Object.keys(tableDef.fields)) {\n provenance.push({ table: tableName, field: fieldName, source: plugin.id });\n }\n }\n }\n }\n\n // Validate foreign key references\n for (const [tableName, tableDef] of Object.entries(merged)) {\n for (const [fieldName, fieldDef] of Object.entries(tableDef.fields)) {\n if (fieldDef.references) {\n // Check if the referenced table exists (by tableName property, not logical name)\n const refTableName = fieldDef.references.table;\n const refExists = Object.values(merged).some(\n (t) => t.tableName === refTableName || Object.keys(merged).includes(refTableName),\n );\n\n if (!refExists) {\n errors.push({\n type: 'invalid_reference',\n message: `Field \"${fieldName}\" on table \"${tableName}\" references table \"${refTableName}\" which does not exist.`,\n table: tableName,\n field: fieldName,\n plugin: tableSources[tableName] || 'unknown',\n });\n }\n }\n }\n }\n\n if (errors.length > 0) {\n const messages = errors.map((e) => ` - [${e.plugin}] ${e.message}`).join('\\n');\n throw new Error(`Schema merge errors:\\n${messages}`);\n }\n\n // Sort tables by order, then by name for stability\n const tables: MergedTable[] = Object.entries(merged)\n .map(([name, definition]) => ({\n name,\n definition,\n source: (tableSources[name] || 'core') as 'core' | string,\n }))\n .sort((a, b) => {\n const orderA = a.definition.order ?? 100;\n const orderB = b.definition.order ?? 100;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n return a.name.localeCompare(b.name);\n });\n\n return { tables, provenance };\n}\n\n// =============================================================================\n// Diff Utilities (for `npx invect generate --diff` preview)\n// =============================================================================\n\nexport interface SchemaDiff {\n newTables: { name: string; source: string }[];\n newFields: { table: string; field: string; source: string }[];\n unchangedTables: string[];\n}\n\n/**\n * Compare a merged schema against a \"previous\" schema (e.g., from last generation).\n * Used for preview/diff output in the CLI.\n */\nexport function diffSchemas(current: MergedSchema, previous: MergedSchema | null): SchemaDiff {\n const diff: SchemaDiff = {\n newTables: [],\n newFields: [],\n unchangedTables: [],\n };\n\n if (!previous) {\n // Everything is new\n diff.newTables = current.tables.map((t) => ({ name: t.name, source: t.source }));\n return diff;\n }\n\n const previousTableNames = new Set(previous.tables.map((t) => t.name));\n const previousFieldsByTable = new Map<string, Set<string>>();\n\n for (const table of previous.tables) {\n previousFieldsByTable.set(table.name, new Set(Object.keys(table.definition.fields)));\n }\n\n for (const table of current.tables) {\n if (!previousTableNames.has(table.name)) {\n diff.newTables.push({ name: table.name, source: table.source });\n continue;\n }\n\n const prevFields = previousFieldsByTable.get(table.name) || new Set();\n let hasNewFields = false;\n\n for (const fieldName of Object.keys(table.definition.fields)) {\n if (!prevFields.has(fieldName)) {\n diff.newFields.push({\n table: table.name,\n field: fieldName,\n source: table.source,\n });\n hasNewFields = true;\n }\n }\n\n if (!hasNewFields) {\n diff.unchangedTables.push(table.name);\n }\n }\n\n return diff;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction deepCopyTableDef(def: PluginTableDefinition): PluginTableDefinition {\n return {\n ...def,\n fields: Object.fromEntries(Object.entries(def.fields).map(([k, v]) => [k, { ...v }])),\n compositePrimaryKey: def.compositePrimaryKey ? [...def.compositePrimaryKey] : undefined,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-merger.js","names":[],"sources":["../../src/database/schema-merger.ts"],"sourcesContent":["/**\n * Schema Merger\n *\n * Merges the core abstract schema with plugin schemas to produce a\n * unified abstract schema. The CLI schema generator then converts\n * this merged schema into dialect-specific Drizzle files.\n *\n * Handles:\n * - New plugin tables\n * - Plugin fields added to existing core tables (additive only)\n * - Duplicate field detection (throws error)\n * - Ordering by foreign key dependencies\n */\n\nimport type { InvectPlugin, PluginTableDefinition } from 'src/types/plugin.types';\nimport { CORE_SCHEMA, CORE_TABLE_NAMES } from './core-schema';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface MergedSchema {\n /** All tables (core + plugin) in dependency-resolved order */\n tables: MergedTable[];\n /** Which plugin contributed each table/field (for diagnostics) */\n provenance: SchemaProvenance[];\n}\n\nexport interface MergedTable {\n /** Logical table name (camelCase) */\n name: string;\n /** The merged table definition */\n definition: PluginTableDefinition;\n /** Which source this table came from */\n source: 'core' | string; // 'core' or plugin ID\n}\n\nexport interface SchemaProvenance {\n /** Table name */\n table: string;\n /** Field name (null for table-level provenance) */\n field: string | null;\n /** Source: 'core' or plugin ID */\n source: string;\n}\n\nexport interface SchemaMergeError {\n type: 'duplicate_field' | 'duplicate_table' | 'invalid_reference';\n message: string;\n table: string;\n field?: string;\n plugin: string;\n}\n\n// =============================================================================\n// Merge Function\n// =============================================================================\n\n/**\n * Merge core schema with all plugin schemas.\n *\n * @param plugins - Array of plugins (only those with `schema` are processed)\n * @returns Merged schema with tables in dependency order\n * @throws Error if there are conflicting field definitions\n */\nexport function mergeSchemas(plugins: InvectPlugin[]): MergedSchema {\n const errors: SchemaMergeError[] = [];\n const provenance: SchemaProvenance[] = [];\n\n // Start with a deep copy of the core schema\n const merged: Record<string, PluginTableDefinition> = {};\n const tableSources: Record<string, string> = {}; // table → source\n\n // Add core tables\n for (const [name, def] of Object.entries(CORE_SCHEMA)) {\n merged[name] = deepCopyTableDef(def);\n tableSources[name] = 'core';\n provenance.push({ table: name, field: null, source: 'core' });\n for (const fieldName of Object.keys(def.fields)) {\n provenance.push({ table: name, field: fieldName, source: 'core' });\n }\n }\n\n // Merge each plugin's schema\n for (const plugin of plugins) {\n if (!plugin.schema) {\n continue;\n }\n\n for (const [tableName, tableDef] of Object.entries(plugin.schema)) {\n if (tableDef.disableMigration) {\n continue;\n }\n\n const _isCoreTable = CORE_TABLE_NAMES.includes(tableName);\n const existsAlready = tableName in merged;\n\n if (existsAlready) {\n // Extending an existing table — merge fields additively\n const existing = merged[tableName];\n if (!existing) {\n continue;\n }\n\n for (const [fieldName, fieldDef] of Object.entries(tableDef.fields)) {\n if (fieldName in existing.fields) {\n // Field already exists — error unless it's from the same source\n const existingSource = provenance.find(\n (p) => p.table === tableName && p.field === fieldName,\n )?.source;\n\n if (existingSource && existingSource !== plugin.id) {\n errors.push({\n type: 'duplicate_field',\n message: `Field \"${fieldName}\" on table \"${tableName}\" already defined by \"${existingSource}\". Plugin \"${plugin.id}\" cannot override it.`,\n table: tableName,\n field: fieldName,\n plugin: plugin.id,\n });\n }\n continue; // Skip duplicate\n }\n\n // Add new field to existing table\n existing.fields[fieldName] = { ...fieldDef };\n provenance.push({ table: tableName, field: fieldName, source: plugin.id });\n }\n } else {\n // New table from plugin\n merged[tableName] = deepCopyTableDef(tableDef);\n tableSources[tableName] = plugin.id;\n provenance.push({ table: tableName, field: null, source: plugin.id });\n\n for (const fieldName of Object.keys(tableDef.fields)) {\n provenance.push({ table: tableName, field: fieldName, source: plugin.id });\n }\n }\n }\n }\n\n // Validate foreign key references\n for (const [tableName, tableDef] of Object.entries(merged)) {\n for (const [fieldName, fieldDef] of Object.entries(tableDef.fields)) {\n if (fieldDef.references) {\n // Check if the referenced table exists (by tableName property, not logical name)\n const refTableName = fieldDef.references.table;\n const refExists = Object.values(merged).some(\n (t) => t.tableName === refTableName || Object.keys(merged).includes(refTableName),\n );\n\n if (!refExists) {\n errors.push({\n type: 'invalid_reference',\n message: `Field \"${fieldName}\" on table \"${tableName}\" references table \"${refTableName}\" which does not exist.`,\n table: tableName,\n field: fieldName,\n plugin: tableSources[tableName] || 'unknown',\n });\n }\n }\n }\n }\n\n if (errors.length > 0) {\n const messages = errors.map((e) => ` - [${e.plugin}] ${e.message}`).join('\\n');\n throw new Error(`Schema merge errors:\\n${messages}`);\n }\n\n // Sort tables by order, then by name for stability\n const tables: MergedTable[] = Object.entries(merged)\n .map(([name, definition]) => ({\n name,\n definition,\n source: (tableSources[name] || 'core') as 'core' | string,\n }))\n .sort((a, b) => {\n const orderA = a.definition.order ?? 100;\n const orderB = b.definition.order ?? 100;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n return a.name.localeCompare(b.name);\n });\n\n return { tables, provenance };\n}\n\n// =============================================================================\n// Diff Utilities (for `npx invect-cli generate --diff` preview)\n// =============================================================================\n\nexport interface SchemaDiff {\n newTables: { name: string; source: string }[];\n newFields: { table: string; field: string; source: string }[];\n unchangedTables: string[];\n}\n\n/**\n * Compare a merged schema against a \"previous\" schema (e.g., from last generation).\n * Used for preview/diff output in the CLI.\n */\nexport function diffSchemas(current: MergedSchema, previous: MergedSchema | null): SchemaDiff {\n const diff: SchemaDiff = {\n newTables: [],\n newFields: [],\n unchangedTables: [],\n };\n\n if (!previous) {\n // Everything is new\n diff.newTables = current.tables.map((t) => ({ name: t.name, source: t.source }));\n return diff;\n }\n\n const previousTableNames = new Set(previous.tables.map((t) => t.name));\n const previousFieldsByTable = new Map<string, Set<string>>();\n\n for (const table of previous.tables) {\n previousFieldsByTable.set(table.name, new Set(Object.keys(table.definition.fields)));\n }\n\n for (const table of current.tables) {\n if (!previousTableNames.has(table.name)) {\n diff.newTables.push({ name: table.name, source: table.source });\n continue;\n }\n\n const prevFields = previousFieldsByTable.get(table.name) || new Set();\n let hasNewFields = false;\n\n for (const fieldName of Object.keys(table.definition.fields)) {\n if (!prevFields.has(fieldName)) {\n diff.newFields.push({\n table: table.name,\n field: fieldName,\n source: table.source,\n });\n hasNewFields = true;\n }\n }\n\n if (!hasNewFields) {\n diff.unchangedTables.push(table.name);\n }\n }\n\n return diff;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction deepCopyTableDef(def: PluginTableDefinition): PluginTableDefinition {\n return {\n ...def,\n fields: Object.fromEntries(Object.entries(def.fields).map(([k, v]) => [k, { ...v }])),\n compositePrimaryKey: def.compositePrimaryKey ? [...def.compositePrimaryKey] : undefined,\n };\n}\n"],"mappings":"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"}
|
|
@@ -50,7 +50,7 @@ async function verifySchema(connection, logger, options = {}) {
|
|
|
50
50
|
...messages,
|
|
51
51
|
"",
|
|
52
52
|
"To fix this, run:",
|
|
53
|
-
" npx invect generate # regenerate schema files",
|
|
53
|
+
" npx invect-cli generate # regenerate schema files",
|
|
54
54
|
" npx drizzle-kit push # apply schema to database (Drizzle)",
|
|
55
55
|
" npx prisma db push # apply schema to database (Prisma)"
|
|
56
56
|
].join("\n");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-verification.cjs","names":["mergeSchemas"],"sources":["../../src/database/schema-verification.ts"],"sourcesContent":["/**\n * Schema Verification\n *\n * Lightweight startup check that verifies the database has the expected\n * tables and columns matching the abstract schema (core + plugins).\n *\n * This does NOT run migrations — the developer is responsible for running\n * `npx invect generate` (CLI) and then applying the schema themselves\n * (e.g., via Drizzle Kit push/migrate, Prisma migrate, or raw SQL).\n *\n * The core calls `verifySchema()` on startup after connecting to the DB.\n * If any required tables or columns are missing, it logs clear warnings\n * (or throws if configured strictly) so the developer knows which\n * schema changes to apply.\n *\n * Dialect-specific introspection:\n * - SQLite: `PRAGMA table_info(<table>)`\n * - PostgreSQL: `information_schema.tables` + `information_schema.columns`\n * - MySQL: `information_schema.tables` + `information_schema.columns`\n */\n\nimport type { DatabaseConnection } from './connection';\nimport type { Logger } from 'src/types/schemas';\nimport { mergeSchemas } from './schema-merger';\nimport type { InvectPlugin } from 'src/types/plugin.types';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface SchemaVerificationResult {\n /** Whether the schema is fully valid (no missing tables or columns) */\n valid: boolean;\n /** Tables that exist in the abstract schema but not in the database */\n missingTables: string[];\n /** Columns that exist in the abstract schema but not in the database */\n missingColumns: { table: string; column: string }[];\n /** Tables that were found and checked */\n verifiedTables: string[];\n}\n\nexport interface SchemaVerificationOptions {\n /**\n * If true, throw an error when the schema is invalid.\n * If false (default), only log warnings.\n */\n strict?: boolean;\n /**\n * Plugins that extend the schema (their tables/columns will also be verified).\n */\n plugins?: InvectPlugin[];\n}\n\n// =============================================================================\n// Main Verification Function\n// =============================================================================\n\n/**\n * Verify that the database has all tables and columns required by the\n * abstract schema (core + plugins).\n *\n * This is called on startup after the database connection is established.\n * It does NOT run migrations — it only checks what exists.\n */\nexport async function verifySchema(\n connection: DatabaseConnection,\n logger: Logger,\n options: SchemaVerificationOptions = {},\n): Promise<SchemaVerificationResult> {\n const merged = mergeSchemas(options.plugins || []);\n const result: SchemaVerificationResult = {\n valid: true,\n missingTables: [],\n missingColumns: [],\n verifiedTables: [],\n };\n\n // Get the actual tables/columns from the database\n const actualSchema = await introspectDatabase(connection);\n\n // Compare against the abstract schema\n for (const table of merged.tables) {\n if (table.definition.disableMigration) {\n continue;\n }\n\n const dbTableName = table.definition.tableName || toSnakeCase(table.name);\n\n if (!actualSchema.has(dbTableName)) {\n result.valid = false;\n result.missingTables.push(dbTableName);\n continue;\n }\n\n result.verifiedTables.push(dbTableName);\n const actualColumns = actualSchema.get(dbTableName) ?? new Set<string>();\n\n for (const [fieldName] of Object.entries(table.definition.fields)) {\n const dbColName = toSnakeCase(fieldName);\n if (!actualColumns.has(dbColName)) {\n result.valid = false;\n result.missingColumns.push({ table: dbTableName, column: dbColName });\n }\n }\n }\n\n // Report results\n if (result.valid) {\n logger.info('Schema verification passed', {\n tablesVerified: result.verifiedTables.length,\n });\n } else {\n const messages: string[] = [];\n\n if (result.missingTables.length > 0) {\n messages.push(`Missing tables: ${result.missingTables.join(', ')}`);\n }\n if (result.missingColumns.length > 0) {\n const colList = result.missingColumns.map((c) => `${c.table}.${c.column}`).join(', ');\n messages.push(`Missing columns: ${colList}`);\n }\n\n const fullMessage = [\n 'Schema verification failed — your database is missing required tables or columns.',\n ...messages,\n '',\n 'To fix this, run:',\n ' npx invect generate # regenerate schema files',\n ' npx drizzle-kit push # apply schema to database (Drizzle)',\n ' npx prisma db push # apply schema to database (Prisma)',\n ].join('\\n');\n\n if (options.strict) {\n logger.error(fullMessage);\n throw new Error(fullMessage);\n } else {\n logger.warn(fullMessage);\n }\n }\n\n return result;\n}\n\n// =============================================================================\n// Database Introspection\n// =============================================================================\n\n/**\n * Introspect the database to discover existing tables and their columns.\n * Returns a Map of tableName → Set<columnName>.\n */\nasync function introspectDatabase(\n connection: DatabaseConnection,\n): Promise<Map<string, Set<string>>> {\n switch (connection.type) {\n case 'sqlite':\n return introspectSqlite(connection.db);\n case 'postgresql':\n return introspectPostgres(connection.db);\n case 'mysql':\n return introspectMysql(connection.db);\n default:\n throw new Error(`Unsupported database type for schema verification`);\n }\n}\n\nasync function introspectSqlite(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n // Use better-sqlite3's synchronous $client.prepare().all() API\n const client = (\n db as unknown as {\n $client: { prepare(sql: string): { all(): Array<Record<string, unknown>> } };\n }\n ).$client;\n\n const tables = client\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__%'`,\n )\n .all() as Array<{ name: string }>;\n\n for (const table of tables) {\n const columns = client.prepare(`PRAGMA table_info('${table.name}')`).all() as Array<{\n name: string;\n }>;\n schema.set(table.name, new Set(columns.map((c) => c.name)));\n }\n\n return schema;\n}\n\nasync function introspectPostgres(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n const result = await (\n db as { execute: (sql: string) => Promise<{ rows?: Array<Record<string, string>> }> }\n ).execute(\n `SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = 'public'\n ORDER BY table_name, ordinal_position`,\n );\n\n for (const row of (result.rows || []) as Array<Record<string, string>>) {\n const tableName = row['table_name'] ?? '';\n const columnName = row['column_name'] ?? '';\n\n if (!schema.has(tableName)) {\n schema.set(tableName, new Set());\n }\n const cols = schema.get(tableName);\n if (cols) {\n cols.add(columnName);\n }\n }\n\n return schema;\n}\n\nasync function introspectMysql(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n const result = await (\n db as {\n execute: (\n sql: string,\n ) => Promise<{ rows?: Array<Record<string, string>> } | Array<Array<Record<string, string>>>>;\n }\n ).execute(\n `SELECT TABLE_NAME, COLUMN_NAME\n FROM information_schema.COLUMNS\n WHERE TABLE_SCHEMA = DATABASE()\n ORDER BY TABLE_NAME, ORDINAL_POSITION`,\n );\n\n const rows = Array.isArray(result)\n ? (((result as Array<unknown>)[0] ?? []) as Array<Record<string, string>>)\n : ((result as { rows?: Array<Record<string, string>> }).rows ?? []);\n\n for (const row of rows) {\n const tableName = row['TABLE_NAME'] ?? '';\n const columnName = row['COLUMN_NAME'] ?? '';\n\n if (!schema.has(tableName)) {\n schema.set(tableName, new Set());\n }\n const cols = schema.get(tableName);\n if (cols) {\n cols.add(columnName);\n }\n }\n\n return schema;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction toSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-verification.cjs","names":["mergeSchemas"],"sources":["../../src/database/schema-verification.ts"],"sourcesContent":["/**\n * Schema Verification\n *\n * Lightweight startup check that verifies the database has the expected\n * tables and columns matching the abstract schema (core + plugins).\n *\n * This does NOT run migrations — the developer is responsible for running\n * `npx invect-cli generate` (CLI) and then applying the schema themselves\n * (e.g., via Drizzle Kit push/migrate, Prisma migrate, or raw SQL).\n *\n * The core calls `verifySchema()` on startup after connecting to the DB.\n * If any required tables or columns are missing, it logs clear warnings\n * (or throws if configured strictly) so the developer knows which\n * schema changes to apply.\n *\n * Dialect-specific introspection:\n * - SQLite: `PRAGMA table_info(<table>)`\n * - PostgreSQL: `information_schema.tables` + `information_schema.columns`\n * - MySQL: `information_schema.tables` + `information_schema.columns`\n */\n\nimport type { DatabaseConnection } from './connection';\nimport type { Logger } from 'src/types/schemas';\nimport { mergeSchemas } from './schema-merger';\nimport type { InvectPlugin } from 'src/types/plugin.types';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface SchemaVerificationResult {\n /** Whether the schema is fully valid (no missing tables or columns) */\n valid: boolean;\n /** Tables that exist in the abstract schema but not in the database */\n missingTables: string[];\n /** Columns that exist in the abstract schema but not in the database */\n missingColumns: { table: string; column: string }[];\n /** Tables that were found and checked */\n verifiedTables: string[];\n}\n\nexport interface SchemaVerificationOptions {\n /**\n * If true, throw an error when the schema is invalid.\n * If false (default), only log warnings.\n */\n strict?: boolean;\n /**\n * Plugins that extend the schema (their tables/columns will also be verified).\n */\n plugins?: InvectPlugin[];\n}\n\n// =============================================================================\n// Main Verification Function\n// =============================================================================\n\n/**\n * Verify that the database has all tables and columns required by the\n * abstract schema (core + plugins).\n *\n * This is called on startup after the database connection is established.\n * It does NOT run migrations — it only checks what exists.\n */\nexport async function verifySchema(\n connection: DatabaseConnection,\n logger: Logger,\n options: SchemaVerificationOptions = {},\n): Promise<SchemaVerificationResult> {\n const merged = mergeSchemas(options.plugins || []);\n const result: SchemaVerificationResult = {\n valid: true,\n missingTables: [],\n missingColumns: [],\n verifiedTables: [],\n };\n\n // Get the actual tables/columns from the database\n const actualSchema = await introspectDatabase(connection);\n\n // Compare against the abstract schema\n for (const table of merged.tables) {\n if (table.definition.disableMigration) {\n continue;\n }\n\n const dbTableName = table.definition.tableName || toSnakeCase(table.name);\n\n if (!actualSchema.has(dbTableName)) {\n result.valid = false;\n result.missingTables.push(dbTableName);\n continue;\n }\n\n result.verifiedTables.push(dbTableName);\n const actualColumns = actualSchema.get(dbTableName) ?? new Set<string>();\n\n for (const [fieldName] of Object.entries(table.definition.fields)) {\n const dbColName = toSnakeCase(fieldName);\n if (!actualColumns.has(dbColName)) {\n result.valid = false;\n result.missingColumns.push({ table: dbTableName, column: dbColName });\n }\n }\n }\n\n // Report results\n if (result.valid) {\n logger.info('Schema verification passed', {\n tablesVerified: result.verifiedTables.length,\n });\n } else {\n const messages: string[] = [];\n\n if (result.missingTables.length > 0) {\n messages.push(`Missing tables: ${result.missingTables.join(', ')}`);\n }\n if (result.missingColumns.length > 0) {\n const colList = result.missingColumns.map((c) => `${c.table}.${c.column}`).join(', ');\n messages.push(`Missing columns: ${colList}`);\n }\n\n const fullMessage = [\n 'Schema verification failed — your database is missing required tables or columns.',\n ...messages,\n '',\n 'To fix this, run:',\n ' npx invect-cli generate # regenerate schema files',\n ' npx drizzle-kit push # apply schema to database (Drizzle)',\n ' npx prisma db push # apply schema to database (Prisma)',\n ].join('\\n');\n\n if (options.strict) {\n logger.error(fullMessage);\n throw new Error(fullMessage);\n } else {\n logger.warn(fullMessage);\n }\n }\n\n return result;\n}\n\n// =============================================================================\n// Database Introspection\n// =============================================================================\n\n/**\n * Introspect the database to discover existing tables and their columns.\n * Returns a Map of tableName → Set<columnName>.\n */\nasync function introspectDatabase(\n connection: DatabaseConnection,\n): Promise<Map<string, Set<string>>> {\n switch (connection.type) {\n case 'sqlite':\n return introspectSqlite(connection.db);\n case 'postgresql':\n return introspectPostgres(connection.db);\n case 'mysql':\n return introspectMysql(connection.db);\n default:\n throw new Error(`Unsupported database type for schema verification`);\n }\n}\n\nasync function introspectSqlite(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n // Use better-sqlite3's synchronous $client.prepare().all() API\n const client = (\n db as unknown as {\n $client: { prepare(sql: string): { all(): Array<Record<string, unknown>> } };\n }\n ).$client;\n\n const tables = client\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__%'`,\n )\n .all() as Array<{ name: string }>;\n\n for (const table of tables) {\n const columns = client.prepare(`PRAGMA table_info('${table.name}')`).all() as Array<{\n name: string;\n }>;\n schema.set(table.name, new Set(columns.map((c) => c.name)));\n }\n\n return schema;\n}\n\nasync function introspectPostgres(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n const result = await (\n db as { execute: (sql: string) => Promise<{ rows?: Array<Record<string, string>> }> }\n ).execute(\n `SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = 'public'\n ORDER BY table_name, ordinal_position`,\n );\n\n for (const row of (result.rows || []) as Array<Record<string, string>>) {\n const tableName = row['table_name'] ?? '';\n const columnName = row['column_name'] ?? '';\n\n if (!schema.has(tableName)) {\n schema.set(tableName, new Set());\n }\n const cols = schema.get(tableName);\n if (cols) {\n cols.add(columnName);\n }\n }\n\n return schema;\n}\n\nasync function introspectMysql(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n const result = await (\n db as {\n execute: (\n sql: string,\n ) => Promise<{ rows?: Array<Record<string, string>> } | Array<Array<Record<string, string>>>>;\n }\n ).execute(\n `SELECT TABLE_NAME, COLUMN_NAME\n FROM information_schema.COLUMNS\n WHERE TABLE_SCHEMA = DATABASE()\n ORDER BY TABLE_NAME, ORDINAL_POSITION`,\n );\n\n const rows = Array.isArray(result)\n ? (((result as Array<unknown>)[0] ?? []) as Array<Record<string, string>>)\n : ((result as { rows?: Array<Record<string, string>> }).rows ?? []);\n\n for (const row of rows) {\n const tableName = row['TABLE_NAME'] ?? '';\n const columnName = row['COLUMN_NAME'] ?? '';\n\n if (!schema.has(tableName)) {\n schema.set(tableName, new Set());\n }\n const cols = schema.get(tableName);\n if (cols) {\n cols.add(columnName);\n }\n }\n\n return schema;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction toSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n"],"mappings":"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"}
|
|
@@ -50,7 +50,7 @@ async function verifySchema(connection, logger, options = {}) {
|
|
|
50
50
|
...messages,
|
|
51
51
|
"",
|
|
52
52
|
"To fix this, run:",
|
|
53
|
-
" npx invect generate # regenerate schema files",
|
|
53
|
+
" npx invect-cli generate # regenerate schema files",
|
|
54
54
|
" npx drizzle-kit push # apply schema to database (Drizzle)",
|
|
55
55
|
" npx prisma db push # apply schema to database (Prisma)"
|
|
56
56
|
].join("\n");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-verification.js","names":[],"sources":["../../src/database/schema-verification.ts"],"sourcesContent":["/**\n * Schema Verification\n *\n * Lightweight startup check that verifies the database has the expected\n * tables and columns matching the abstract schema (core + plugins).\n *\n * This does NOT run migrations — the developer is responsible for running\n * `npx invect generate` (CLI) and then applying the schema themselves\n * (e.g., via Drizzle Kit push/migrate, Prisma migrate, or raw SQL).\n *\n * The core calls `verifySchema()` on startup after connecting to the DB.\n * If any required tables or columns are missing, it logs clear warnings\n * (or throws if configured strictly) so the developer knows which\n * schema changes to apply.\n *\n * Dialect-specific introspection:\n * - SQLite: `PRAGMA table_info(<table>)`\n * - PostgreSQL: `information_schema.tables` + `information_schema.columns`\n * - MySQL: `information_schema.tables` + `information_schema.columns`\n */\n\nimport type { DatabaseConnection } from './connection';\nimport type { Logger } from 'src/types/schemas';\nimport { mergeSchemas } from './schema-merger';\nimport type { InvectPlugin } from 'src/types/plugin.types';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface SchemaVerificationResult {\n /** Whether the schema is fully valid (no missing tables or columns) */\n valid: boolean;\n /** Tables that exist in the abstract schema but not in the database */\n missingTables: string[];\n /** Columns that exist in the abstract schema but not in the database */\n missingColumns: { table: string; column: string }[];\n /** Tables that were found and checked */\n verifiedTables: string[];\n}\n\nexport interface SchemaVerificationOptions {\n /**\n * If true, throw an error when the schema is invalid.\n * If false (default), only log warnings.\n */\n strict?: boolean;\n /**\n * Plugins that extend the schema (their tables/columns will also be verified).\n */\n plugins?: InvectPlugin[];\n}\n\n// =============================================================================\n// Main Verification Function\n// =============================================================================\n\n/**\n * Verify that the database has all tables and columns required by the\n * abstract schema (core + plugins).\n *\n * This is called on startup after the database connection is established.\n * It does NOT run migrations — it only checks what exists.\n */\nexport async function verifySchema(\n connection: DatabaseConnection,\n logger: Logger,\n options: SchemaVerificationOptions = {},\n): Promise<SchemaVerificationResult> {\n const merged = mergeSchemas(options.plugins || []);\n const result: SchemaVerificationResult = {\n valid: true,\n missingTables: [],\n missingColumns: [],\n verifiedTables: [],\n };\n\n // Get the actual tables/columns from the database\n const actualSchema = await introspectDatabase(connection);\n\n // Compare against the abstract schema\n for (const table of merged.tables) {\n if (table.definition.disableMigration) {\n continue;\n }\n\n const dbTableName = table.definition.tableName || toSnakeCase(table.name);\n\n if (!actualSchema.has(dbTableName)) {\n result.valid = false;\n result.missingTables.push(dbTableName);\n continue;\n }\n\n result.verifiedTables.push(dbTableName);\n const actualColumns = actualSchema.get(dbTableName) ?? new Set<string>();\n\n for (const [fieldName] of Object.entries(table.definition.fields)) {\n const dbColName = toSnakeCase(fieldName);\n if (!actualColumns.has(dbColName)) {\n result.valid = false;\n result.missingColumns.push({ table: dbTableName, column: dbColName });\n }\n }\n }\n\n // Report results\n if (result.valid) {\n logger.info('Schema verification passed', {\n tablesVerified: result.verifiedTables.length,\n });\n } else {\n const messages: string[] = [];\n\n if (result.missingTables.length > 0) {\n messages.push(`Missing tables: ${result.missingTables.join(', ')}`);\n }\n if (result.missingColumns.length > 0) {\n const colList = result.missingColumns.map((c) => `${c.table}.${c.column}`).join(', ');\n messages.push(`Missing columns: ${colList}`);\n }\n\n const fullMessage = [\n 'Schema verification failed — your database is missing required tables or columns.',\n ...messages,\n '',\n 'To fix this, run:',\n ' npx invect generate # regenerate schema files',\n ' npx drizzle-kit push # apply schema to database (Drizzle)',\n ' npx prisma db push # apply schema to database (Prisma)',\n ].join('\\n');\n\n if (options.strict) {\n logger.error(fullMessage);\n throw new Error(fullMessage);\n } else {\n logger.warn(fullMessage);\n }\n }\n\n return result;\n}\n\n// =============================================================================\n// Database Introspection\n// =============================================================================\n\n/**\n * Introspect the database to discover existing tables and their columns.\n * Returns a Map of tableName → Set<columnName>.\n */\nasync function introspectDatabase(\n connection: DatabaseConnection,\n): Promise<Map<string, Set<string>>> {\n switch (connection.type) {\n case 'sqlite':\n return introspectSqlite(connection.db);\n case 'postgresql':\n return introspectPostgres(connection.db);\n case 'mysql':\n return introspectMysql(connection.db);\n default:\n throw new Error(`Unsupported database type for schema verification`);\n }\n}\n\nasync function introspectSqlite(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n // Use better-sqlite3's synchronous $client.prepare().all() API\n const client = (\n db as unknown as {\n $client: { prepare(sql: string): { all(): Array<Record<string, unknown>> } };\n }\n ).$client;\n\n const tables = client\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__%'`,\n )\n .all() as Array<{ name: string }>;\n\n for (const table of tables) {\n const columns = client.prepare(`PRAGMA table_info('${table.name}')`).all() as Array<{\n name: string;\n }>;\n schema.set(table.name, new Set(columns.map((c) => c.name)));\n }\n\n return schema;\n}\n\nasync function introspectPostgres(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n const result = await (\n db as { execute: (sql: string) => Promise<{ rows?: Array<Record<string, string>> }> }\n ).execute(\n `SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = 'public'\n ORDER BY table_name, ordinal_position`,\n );\n\n for (const row of (result.rows || []) as Array<Record<string, string>>) {\n const tableName = row['table_name'] ?? '';\n const columnName = row['column_name'] ?? '';\n\n if (!schema.has(tableName)) {\n schema.set(tableName, new Set());\n }\n const cols = schema.get(tableName);\n if (cols) {\n cols.add(columnName);\n }\n }\n\n return schema;\n}\n\nasync function introspectMysql(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n const result = await (\n db as {\n execute: (\n sql: string,\n ) => Promise<{ rows?: Array<Record<string, string>> } | Array<Array<Record<string, string>>>>;\n }\n ).execute(\n `SELECT TABLE_NAME, COLUMN_NAME\n FROM information_schema.COLUMNS\n WHERE TABLE_SCHEMA = DATABASE()\n ORDER BY TABLE_NAME, ORDINAL_POSITION`,\n );\n\n const rows = Array.isArray(result)\n ? (((result as Array<unknown>)[0] ?? []) as Array<Record<string, string>>)\n : ((result as { rows?: Array<Record<string, string>> }).rows ?? []);\n\n for (const row of rows) {\n const tableName = row['TABLE_NAME'] ?? '';\n const columnName = row['COLUMN_NAME'] ?? '';\n\n if (!schema.has(tableName)) {\n schema.set(tableName, new Set());\n }\n const cols = schema.get(tableName);\n if (cols) {\n cols.add(columnName);\n }\n }\n\n return schema;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction toSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-verification.js","names":[],"sources":["../../src/database/schema-verification.ts"],"sourcesContent":["/**\n * Schema Verification\n *\n * Lightweight startup check that verifies the database has the expected\n * tables and columns matching the abstract schema (core + plugins).\n *\n * This does NOT run migrations — the developer is responsible for running\n * `npx invect-cli generate` (CLI) and then applying the schema themselves\n * (e.g., via Drizzle Kit push/migrate, Prisma migrate, or raw SQL).\n *\n * The core calls `verifySchema()` on startup after connecting to the DB.\n * If any required tables or columns are missing, it logs clear warnings\n * (or throws if configured strictly) so the developer knows which\n * schema changes to apply.\n *\n * Dialect-specific introspection:\n * - SQLite: `PRAGMA table_info(<table>)`\n * - PostgreSQL: `information_schema.tables` + `information_schema.columns`\n * - MySQL: `information_schema.tables` + `information_schema.columns`\n */\n\nimport type { DatabaseConnection } from './connection';\nimport type { Logger } from 'src/types/schemas';\nimport { mergeSchemas } from './schema-merger';\nimport type { InvectPlugin } from 'src/types/plugin.types';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface SchemaVerificationResult {\n /** Whether the schema is fully valid (no missing tables or columns) */\n valid: boolean;\n /** Tables that exist in the abstract schema but not in the database */\n missingTables: string[];\n /** Columns that exist in the abstract schema but not in the database */\n missingColumns: { table: string; column: string }[];\n /** Tables that were found and checked */\n verifiedTables: string[];\n}\n\nexport interface SchemaVerificationOptions {\n /**\n * If true, throw an error when the schema is invalid.\n * If false (default), only log warnings.\n */\n strict?: boolean;\n /**\n * Plugins that extend the schema (their tables/columns will also be verified).\n */\n plugins?: InvectPlugin[];\n}\n\n// =============================================================================\n// Main Verification Function\n// =============================================================================\n\n/**\n * Verify that the database has all tables and columns required by the\n * abstract schema (core + plugins).\n *\n * This is called on startup after the database connection is established.\n * It does NOT run migrations — it only checks what exists.\n */\nexport async function verifySchema(\n connection: DatabaseConnection,\n logger: Logger,\n options: SchemaVerificationOptions = {},\n): Promise<SchemaVerificationResult> {\n const merged = mergeSchemas(options.plugins || []);\n const result: SchemaVerificationResult = {\n valid: true,\n missingTables: [],\n missingColumns: [],\n verifiedTables: [],\n };\n\n // Get the actual tables/columns from the database\n const actualSchema = await introspectDatabase(connection);\n\n // Compare against the abstract schema\n for (const table of merged.tables) {\n if (table.definition.disableMigration) {\n continue;\n }\n\n const dbTableName = table.definition.tableName || toSnakeCase(table.name);\n\n if (!actualSchema.has(dbTableName)) {\n result.valid = false;\n result.missingTables.push(dbTableName);\n continue;\n }\n\n result.verifiedTables.push(dbTableName);\n const actualColumns = actualSchema.get(dbTableName) ?? new Set<string>();\n\n for (const [fieldName] of Object.entries(table.definition.fields)) {\n const dbColName = toSnakeCase(fieldName);\n if (!actualColumns.has(dbColName)) {\n result.valid = false;\n result.missingColumns.push({ table: dbTableName, column: dbColName });\n }\n }\n }\n\n // Report results\n if (result.valid) {\n logger.info('Schema verification passed', {\n tablesVerified: result.verifiedTables.length,\n });\n } else {\n const messages: string[] = [];\n\n if (result.missingTables.length > 0) {\n messages.push(`Missing tables: ${result.missingTables.join(', ')}`);\n }\n if (result.missingColumns.length > 0) {\n const colList = result.missingColumns.map((c) => `${c.table}.${c.column}`).join(', ');\n messages.push(`Missing columns: ${colList}`);\n }\n\n const fullMessage = [\n 'Schema verification failed — your database is missing required tables or columns.',\n ...messages,\n '',\n 'To fix this, run:',\n ' npx invect-cli generate # regenerate schema files',\n ' npx drizzle-kit push # apply schema to database (Drizzle)',\n ' npx prisma db push # apply schema to database (Prisma)',\n ].join('\\n');\n\n if (options.strict) {\n logger.error(fullMessage);\n throw new Error(fullMessage);\n } else {\n logger.warn(fullMessage);\n }\n }\n\n return result;\n}\n\n// =============================================================================\n// Database Introspection\n// =============================================================================\n\n/**\n * Introspect the database to discover existing tables and their columns.\n * Returns a Map of tableName → Set<columnName>.\n */\nasync function introspectDatabase(\n connection: DatabaseConnection,\n): Promise<Map<string, Set<string>>> {\n switch (connection.type) {\n case 'sqlite':\n return introspectSqlite(connection.db);\n case 'postgresql':\n return introspectPostgres(connection.db);\n case 'mysql':\n return introspectMysql(connection.db);\n default:\n throw new Error(`Unsupported database type for schema verification`);\n }\n}\n\nasync function introspectSqlite(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n // Use better-sqlite3's synchronous $client.prepare().all() API\n const client = (\n db as unknown as {\n $client: { prepare(sql: string): { all(): Array<Record<string, unknown>> } };\n }\n ).$client;\n\n const tables = client\n .prepare(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__%'`,\n )\n .all() as Array<{ name: string }>;\n\n for (const table of tables) {\n const columns = client.prepare(`PRAGMA table_info('${table.name}')`).all() as Array<{\n name: string;\n }>;\n schema.set(table.name, new Set(columns.map((c) => c.name)));\n }\n\n return schema;\n}\n\nasync function introspectPostgres(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n const result = await (\n db as { execute: (sql: string) => Promise<{ rows?: Array<Record<string, string>> }> }\n ).execute(\n `SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = 'public'\n ORDER BY table_name, ordinal_position`,\n );\n\n for (const row of (result.rows || []) as Array<Record<string, string>>) {\n const tableName = row['table_name'] ?? '';\n const columnName = row['column_name'] ?? '';\n\n if (!schema.has(tableName)) {\n schema.set(tableName, new Set());\n }\n const cols = schema.get(tableName);\n if (cols) {\n cols.add(columnName);\n }\n }\n\n return schema;\n}\n\nasync function introspectMysql(db: DatabaseConnection['db']): Promise<Map<string, Set<string>>> {\n const schema = new Map<string, Set<string>>();\n\n const result = await (\n db as {\n execute: (\n sql: string,\n ) => Promise<{ rows?: Array<Record<string, string>> } | Array<Array<Record<string, string>>>>;\n }\n ).execute(\n `SELECT TABLE_NAME, COLUMN_NAME\n FROM information_schema.COLUMNS\n WHERE TABLE_SCHEMA = DATABASE()\n ORDER BY TABLE_NAME, ORDINAL_POSITION`,\n );\n\n const rows = Array.isArray(result)\n ? (((result as Array<unknown>)[0] ?? []) as Array<Record<string, string>>)\n : ((result as { rows?: Array<Record<string, string>> }).rows ?? []);\n\n for (const row of rows) {\n const tableName = row['TABLE_NAME'] ?? '';\n const columnName = row['COLUMN_NAME'] ?? '';\n\n if (!schema.has(tableName)) {\n schema.set(tableName, new Set());\n }\n const cols = schema.get(tableName);\n if (cols) {\n cols.add(columnName);\n }\n }\n\n return schema;\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction toSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n"],"mappings":"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"}
|
|
@@ -293,7 +293,7 @@ var DatabaseService = class DatabaseService {
|
|
|
293
293
|
if (allMissing) lines.push("Your database exists but has no Invect tables.", "This usually means you haven't pushed the schema yet.");
|
|
294
294
|
else lines.push(`Your database is missing ${missingTables.length} of ${expectedTables.length} required Invect tables:`, ` Missing: ${missingTables.join(", ")}`, "", "This usually means your schema is out of date.");
|
|
295
295
|
lines.push("");
|
|
296
|
-
lines.push("To fix this, run:", "", " npx invect generate # generate schema files (core + plugins)", " npx drizzle-kit push # push schema to the database", "", "Or if you use migrations:", "", " npx invect generate # generate schema files", " npx drizzle-kit generate", " npx invect migrate # apply migrations", "", "The Invect CLI reads your invect.config.ts to discover installed", "plugins and generates the correct schema for all of them.", "");
|
|
296
|
+
lines.push("To fix this, run:", "", " npx invect-cli generate # generate schema files (core + plugins)", " npx drizzle-kit push # push schema to the database", "", "Or if you use migrations:", "", " npx invect-cli generate # generate schema files", " npx drizzle-kit generate", " npx invect-cli migrate # apply migrations", "", "The Invect CLI reads your invect.config.ts to discover installed", "plugins and generates the correct schema for all of them.", "");
|
|
297
297
|
const message = lines.join("\n");
|
|
298
298
|
this.logger.error(message);
|
|
299
299
|
throw new require_errors_types.DatabaseError(`Database is missing ${allMissing ? "all" : missingTables.length} required Invect table(s). Run schema migrations before starting the server. See logs above for instructions.`, { missingTables });
|
|
@@ -347,8 +347,8 @@ var DatabaseService = class DatabaseService {
|
|
|
347
347
|
if (plugin.setupInstructions) lines.push(` Fix: ${plugin.setupInstructions}`);
|
|
348
348
|
lines.push("");
|
|
349
349
|
}
|
|
350
|
-
if (!pluginsWithMissing.some((p) => p.setupInstructions)) lines.push("To fix this, run:", "", " npx invect generate # generate schema files (core + plugins)", " npx drizzle-kit push # push schema to the database", "", "The Invect CLI reads your invect.config.ts, discovers all plugins", "and their required tables, and generates the complete schema.");
|
|
351
|
-
lines.push("", "If a plugin defines a schema, `npx invect generate` will include it", "automatically. For plugins with externally-managed tables, see the", "plugin's README for additional schema setup instructions.", "");
|
|
350
|
+
if (!pluginsWithMissing.some((p) => p.setupInstructions)) lines.push("To fix this, run:", "", " npx invect-cli generate # generate schema files (core + plugins)", " npx drizzle-kit push # push schema to the database", "", "The Invect CLI reads your invect.config.ts, discovers all plugins", "and their required tables, and generates the complete schema.");
|
|
351
|
+
lines.push("", "If a plugin defines a schema, `npx invect-cli generate` will include it", "automatically. For plugins with externally-managed tables, see the", "plugin's README for additional schema setup instructions.", "");
|
|
352
352
|
const message = lines.join("\n");
|
|
353
353
|
this.logger.error(message);
|
|
354
354
|
throw new require_errors_types.DatabaseError(`Database is missing ${totalMissing} table(s) required by plugin(s): ` + pluginsWithMissing.map((p) => `${p.pluginName} (${p.missingTables.join(", ")})`).join("; ") + `. Push the schema before starting the server. See logs above for instructions.`, { pluginsWithMissing: pluginsWithMissing.map((p) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.service.cjs","names":["DatabaseError","DatabaseConnectionFactory","createAdapterFromConnection","verifySchema","CORE_SCHEMA","FlowsModel","FlowVersionsModel","FlowRunsModel","NodeExecutionsModel","BatchJobsModel","AgentToolExecutionsModel","FlowTriggersModel","ChatMessagesModel"],"sources":["../../../src/services/database/database.service.ts"],"sourcesContent":["// Framework-agnostic Database Service for Invect core\n\nimport { DatabaseConnectionFactory, type DatabaseConnection } from '../../database/connection';\nimport { verifySchema, type SchemaVerificationOptions } from '../../database/schema-verification';\nimport { CORE_SCHEMA } from '../../database/core-schema';\nimport { DatabaseError } from 'src/types/common/errors.types';\nimport { FlowRunsModel } from '../flow-runs/flow-runs.model';\nimport { FlowsModel } from '../flows/flows.model';\nimport { BatchJobsModel } from '../batch-jobs/batch-jobs.model';\nimport { FlowVersionsModel } from '../flow-versions/flow-versions.model';\nimport { NodeExecutionsModel } from '../node-executions/node-executions.model';\nimport { AgentToolExecutionsModel } from '../agent-tool-executions/agent-tool-executions.model';\nimport { FlowTriggersModel } from '../triggers/flow-triggers.model';\nimport { ChatMessagesModel } from '../chat/chat-messages.model';\nimport { InvectDatabaseConfig, Logger } from 'src/types/schemas';\nimport type { InvectPlugin } from 'src/types/plugin.types';\nimport type { InvectAdapter } from '../../database/adapter';\nimport { createAdapterFromConnection } from '../../database/adapters/connection-bridge';\n\ntype PostgreSqlClientLike = {\n <T = Record<string, unknown>>(query: string): Promise<T[]>;\n (strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;\n};\n\ntype SqliteClientLike = {\n prepare(sql: string): {\n all(...params: unknown[]): Record<string, unknown>[];\n run(...params: unknown[]): { changes: number };\n get(...params: unknown[]): Record<string, unknown> | undefined;\n };\n exec(sql: string): void;\n};\n\ntype MysqlClientLike = {\n execute<T = Record<string, unknown>>(\n query: string,\n params?: unknown[],\n ): Promise<[T[] | unknown, unknown]>;\n};\n\ntype PostgreSqlDbLike = {\n $client: PostgreSqlClientLike;\n};\n\ntype SqliteDbLike = {\n $client: SqliteClientLike;\n};\n\ntype MysqlDbLike = {\n $client: MysqlClientLike;\n};\n\n/**\n * Describes tables that a plugin needs checked at startup.\n */\nexport interface PluginTableRequirement {\n pluginId: string;\n pluginName: string;\n tables: string[];\n setupInstructions?: string;\n}\n\n/**\n * Core database service implementation\n */\nexport class DatabaseService {\n private connection: DatabaseConnection | null = null;\n private _database: Database | null = null;\n private _adapter: InvectAdapter | null = null;\n private schemaVerificationOptions?: SchemaVerificationOptions;\n private pluginTableRequirements: PluginTableRequirement[] = [];\n\n constructor(\n private readonly hostDbConfig: InvectDatabaseConfig,\n private readonly logger: Logger = console,\n schemaVerification?: SchemaVerificationOptions,\n plugins?: InvectPlugin[],\n ) {\n this.schemaVerificationOptions = schemaVerification;\n this.pluginTableRequirements = DatabaseService.extractPluginTableRequirements(plugins ?? []);\n }\n\n /**\n * Get the Database instance with all model classes\n */\n get database(): Database {\n if (!this._database) {\n throw new DatabaseError('Database not initialized - call initialize() first');\n }\n return this._database;\n }\n\n /**\n * Get the InvectAdapter instance for direct adapter access\n */\n get adapter(): InvectAdapter {\n if (!this._adapter) {\n throw new DatabaseError('Database not initialized - call initialize() first');\n }\n return this._adapter;\n }\n\n /**\n * Direct access to flows model\n */\n get flows() {\n return this.database.flows;\n }\n\n /**\n * Direct access to flow versions model\n */\n get flowVersions() {\n return this.database.flowVersions;\n }\n\n /**\n * Direct access to flow executions model\n */\n get flowRuns() {\n return this.database.flowRuns;\n }\n\n /**\n * Direct access to execution traces model\n */\n get nodeExecutions() {\n return this.database.executionTraces;\n }\n\n /**\n * Direct access to batch jobs model\n */\n get batchJobs() {\n return this.database.batchJobs;\n }\n\n /**\n * Direct access to agent tool executions model\n */\n get agentToolExecutions() {\n return this.database.agentToolExecutions;\n }\n\n /**\n * Direct access to flow triggers model\n */\n get flowTriggers() {\n return this.database.flowTriggers;\n }\n\n /**\n * Direct access to chat messages model\n */\n get chatMessages() {\n return this.database.chatMessages;\n }\n\n /**\n * Initialize the database connection and models\n */\n async initialize(): Promise<void> {\n if (this.connection) {\n this.logger.debug('Database connection already initialized');\n return;\n }\n\n // --- Step 1: Establish the database connection ---\n try {\n this.connection = await DatabaseConnectionFactory.createHostDBConnection(\n this.hostDbConfig,\n this.logger,\n );\n } catch (error) {\n this.logConnectionError(error);\n throw new DatabaseError(\n `Failed to connect to the database: ${error instanceof Error ? error.message : error}`,\n { error, originalStack: error instanceof Error ? error.stack : undefined },\n );\n }\n\n // --- Step 2: Verify connectivity with a simple query ---\n try {\n await this.runConnectivityCheck(this.connection);\n } catch (error) {\n this.logConnectivityError(error);\n throw new DatabaseError(\n `Database connectivity check failed: ${error instanceof Error ? error.message : error}`,\n { error },\n );\n }\n\n // --- Step 3: Check that core Invect tables exist ---\n // This always runs (regardless of schemaVerification config) to catch\n // the common case of a fresh database that hasn't had migrations applied.\n await this.runCoreTableCheck(this.connection);\n\n // --- Step 3b: Check that plugin-required tables exist ---\n // Each plugin can declare requiredTables to get a clear error at startup\n // instead of a cryptic runtime crash when a table is missing.\n await this.runPluginTableChecks(this.connection);\n\n // --- Step 4: Initialize the database models ---\n this._adapter = createAdapterFromConnection(this.connection);\n this._database = new Database(this.connection, this.logger, this._adapter);\n\n // --- Step 5: Run detailed schema verification (opt-in) ---\n // This checks columns, not just tables. Controlled by config.schemaVerification.\n if (this.schemaVerificationOptions) {\n await verifySchema(this.connection, this.logger, this.schemaVerificationOptions);\n }\n\n this.logger.info('Database service initialized successfully');\n }\n\n /**\n * Get the database connection for sharing with other services\n */\n getConnection(): DatabaseConnection {\n if (!this.connection) {\n throw new DatabaseError('Database not initialized - call initialize() first');\n }\n return this.connection;\n }\n\n /**\n * Execute a SQL query on a provided queryDatabase\n */\n async executeQuery(\n query: string,\n queryDBConfig: InvectDatabaseConfig,\n ): Promise<Record<string, unknown>[]> {\n try {\n this.logger.debug('Executing query on external database', {\n query,\n dbType: queryDBConfig.type,\n dbId: queryDBConfig.id,\n });\n\n // Create or get existing query database connection\n const queryConnection = await DatabaseConnectionFactory.createQueryDbConnection(\n queryDBConfig,\n this.logger,\n );\n\n // Execute the query based on database type and return results\n let result: Record<string, unknown>[] = [];\n\n switch (queryDBConfig.type) {\n case 'postgresql': {\n // PostgreSQL query execution using postgres.js client directly\n const client = (queryConnection.db as unknown as PostgreSqlDbLike).$client;\n result = await client<Record<string, unknown>>(query);\n break;\n }\n case 'sqlite': {\n // SQLite query execution using better-sqlite3 synchronous API\n const sqliteClient = (queryConnection.db as unknown as SqliteDbLike).$client;\n result = sqliteClient.prepare(query).all() as Record<string, unknown>[];\n break;\n }\n case 'mysql': {\n // MySQL query execution using mysql2 client directly\n const client = (queryConnection.db as unknown as MysqlDbLike).$client;\n const [rows] = await client.execute<Record<string, unknown>>(query);\n result = Array.isArray(rows) ? rows : [];\n break;\n }\n default:\n throw new DatabaseError(`Unsupported database type: ${queryDBConfig.type}`);\n }\n\n this.logger.debug('Query executed successfully', {\n rowCount: Array.isArray(result) ? result.length : 'unknown',\n dbType: queryDBConfig.type,\n dbId: queryDBConfig.id,\n });\n\n return result;\n } catch (error) {\n this.logger.error('Failed to execute query on external database', {\n error: error instanceof Error ? error.message : error,\n query,\n dbType: queryDBConfig.type,\n dbId: queryDBConfig.id,\n stack: error instanceof Error ? error.stack : undefined,\n });\n\n throw new DatabaseError(\n `Query execution failed on ${queryDBConfig.type} database (${queryDBConfig.id}): ${\n error instanceof Error ? error.message : error\n }`,\n {\n error,\n query,\n dbConfig: queryDBConfig,\n originalStack: error instanceof Error ? error.stack : undefined,\n },\n );\n }\n }\n\n /**\n * Health check method\n */\n async healthCheck(): Promise<void> {\n this.logger.debug('Performing database health check');\n\n if (!this.connection) {\n throw new DatabaseError('Database not initialized - call initialize() first');\n }\n\n try {\n // Simple connectivity test using host database connection\n switch (this.connection.type) {\n case 'postgresql': {\n const client = (this.connection.db as unknown as PostgreSqlDbLike).$client;\n await client`SELECT 1 as health`;\n break;\n }\n case 'sqlite': {\n const client = (this.connection.db as unknown as SqliteDbLike).$client;\n client.prepare('SELECT 1 as health').get();\n break;\n }\n case 'mysql': {\n const client = (this.connection.db as unknown as MysqlDbLike).$client;\n await client.execute('SELECT 1 as health');\n break;\n }\n default:\n throw new DatabaseError('Unsupported database type for health check');\n }\n\n this.logger.debug('Database health check passed');\n } catch (error) {\n this.logger.error('Database health check failed', error);\n throw new DatabaseError('Database health check failed', { error });\n }\n }\n\n /**\n * Close the database connection\n */\n async close(): Promise<void> {\n if (this.connection) {\n this.logger.debug('Closing database connection');\n this.connection = null;\n this._database = null;\n this.logger.info('Database connection closed');\n }\n }\n\n // ===========================================================================\n // Plugin table requirement extraction\n // ===========================================================================\n\n /**\n * Extract table requirements from plugin declarations.\n *\n * A plugin can declare required tables via:\n * 1. `requiredTables: string[]` — explicit list (preferred)\n * 2. `schema: { tableName: ... }` — inferred from abstract schema definitions\n *\n * If both are present, `requiredTables` takes precedence.\n */\n static extractPluginTableRequirements(plugins: InvectPlugin[]): PluginTableRequirement[] {\n const requirements: PluginTableRequirement[] = [];\n\n for (const plugin of plugins) {\n let tables: string[] = [];\n\n if (plugin.requiredTables && plugin.requiredTables.length > 0) {\n // Explicit declaration takes priority\n tables = [...plugin.requiredTables];\n } else if (plugin.schema) {\n // Infer from abstract schema — each entry's tableName (or snake_case key)\n for (const [key, def] of Object.entries(plugin.schema)) {\n const tableName = (def as { tableName?: string }).tableName ?? key;\n const disabled = (def as { disableMigration?: boolean }).disableMigration;\n if (!disabled) {\n tables.push(tableName);\n }\n }\n }\n\n if (tables.length > 0) {\n requirements.push({\n pluginId: plugin.id,\n pluginName: plugin.name ?? plugin.id,\n tables,\n setupInstructions: plugin.setupInstructions,\n });\n }\n }\n\n return requirements;\n }\n\n // ===========================================================================\n // Startup check helpers\n // ===========================================================================\n\n /**\n * Run a simple `SELECT 1` query to verify the database is reachable.\n */\n private async runConnectivityCheck(connection: DatabaseConnection): Promise<void> {\n switch (connection.type) {\n case 'postgresql': {\n const client = (connection.db as unknown as PostgreSqlDbLike).$client;\n await client`SELECT 1 as health`;\n break;\n }\n case 'sqlite': {\n const client = (connection.db as unknown as SqliteDbLike).$client;\n client.prepare('SELECT 1 as health').get();\n break;\n }\n case 'mysql': {\n const client = (connection.db as unknown as MysqlDbLike).$client;\n await client.execute('SELECT 1 as health');\n break;\n }\n }\n }\n\n /**\n * Check that the essential Invect tables exist in the database.\n * This catches the most common developer mistake: running the app\n * before applying the database schema.\n *\n * Only checks for table *existence*, not column correctness (that's\n * what the opt-in `schemaVerification` does).\n */\n private async runCoreTableCheck(connection: DatabaseConnection): Promise<void> {\n // Collect the expected core table names from the abstract schema\n const expectedTables: string[] = [];\n for (const def of Object.values(CORE_SCHEMA)) {\n const tableDef = def as { tableName?: string; disableMigration?: boolean };\n if (tableDef.disableMigration) {\n continue;\n }\n if (tableDef.tableName) {\n expectedTables.push(tableDef.tableName);\n }\n }\n\n // Get actual table names from the database\n let actualTableNames: Set<string>;\n try {\n actualTableNames = await this.listTableNames(connection);\n } catch (error) {\n // If we can't even list tables, the database is probably unreachable\n // or misconfigured — the connectivity check should have caught this,\n // but log and proceed gracefully.\n this.logger.warn('Could not introspect database tables. Skipping startup table check.', {\n error: error instanceof Error ? error.message : error,\n });\n return;\n }\n\n const missingTables = expectedTables.filter((t) => !actualTableNames.has(t));\n\n if (missingTables.length === 0) {\n this.logger.debug('Core table check passed', {\n tablesFound: expectedTables.length,\n });\n return;\n }\n\n // --- Build a helpful error message ---\n const allMissing = missingTables.length === expectedTables.length;\n\n const lines: string[] = [\n '',\n '╔══════════════════════════════════════════════════════════════╗',\n '║ ⚠ INVECT — DATABASE NOT READY ⚠ ║',\n '╚══════════════════════════════════════════════════════════════╝',\n '',\n ];\n\n if (allMissing) {\n lines.push(\n 'Your database exists but has no Invect tables.',\n \"This usually means you haven't pushed the schema yet.\",\n );\n } else {\n lines.push(\n `Your database is missing ${missingTables.length} of ${expectedTables.length} required Invect tables:`,\n ` Missing: ${missingTables.join(', ')}`,\n '',\n 'This usually means your schema is out of date.',\n );\n }\n\n lines.push('');\n\n // Fix instructions — point users to the Invect CLI\n lines.push(\n 'To fix this, run:',\n '',\n ' npx invect generate # generate schema files (core + plugins)',\n ' npx drizzle-kit push # push schema to the database',\n '',\n 'Or if you use migrations:',\n '',\n ' npx invect generate # generate schema files',\n ' npx drizzle-kit generate',\n ' npx invect migrate # apply migrations',\n '',\n 'The Invect CLI reads your invect.config.ts to discover installed',\n 'plugins and generates the correct schema for all of them.',\n '',\n );\n\n const message = lines.join('\\n');\n this.logger.error(message);\n\n throw new DatabaseError(\n `Database is missing ${allMissing ? 'all' : missingTables.length} required Invect table(s). ` +\n `Run schema migrations before starting the server. See logs above for instructions.`,\n { missingTables },\n );\n }\n\n /**\n * Check that tables required by plugins exist in the database.\n *\n * Each plugin can declare `requiredTables` (explicit list) or have them\n * inferred from its `schema` definition. This method checks all of them\n * in a single pass and produces a clear, attributed error message so the\n * developer knows exactly which plugin needs which tables.\n */\n private async runPluginTableChecks(connection: DatabaseConnection): Promise<void> {\n if (this.pluginTableRequirements.length === 0) {\n return; // No plugins declared required tables\n }\n\n // Get actual table names from the database\n let actualTableNames: Set<string>;\n try {\n actualTableNames = await this.listTableNames(connection);\n } catch {\n // If introspection fails, skip gracefully (core check already warned)\n return;\n }\n\n // Collect all missing tables grouped by plugin\n const pluginsWithMissing: Array<{\n pluginId: string;\n pluginName: string;\n missingTables: string[];\n setupInstructions?: string;\n }> = [];\n\n for (const req of this.pluginTableRequirements) {\n const missing = req.tables.filter((t) => !actualTableNames.has(t));\n if (missing.length > 0) {\n pluginsWithMissing.push({\n pluginId: req.pluginId,\n pluginName: req.pluginName,\n missingTables: missing,\n setupInstructions: req.setupInstructions,\n });\n }\n }\n\n if (pluginsWithMissing.length === 0) {\n const totalTables = this.pluginTableRequirements.reduce((sum, r) => sum + r.tables.length, 0);\n this.logger.debug('Plugin table check passed', {\n plugins: this.pluginTableRequirements.length,\n tablesChecked: totalTables,\n });\n return;\n }\n\n // --- Build a helpful, plugin-attributed error message ---\n const totalMissing = pluginsWithMissing.reduce((sum, p) => sum + p.missingTables.length, 0);\n\n const lines: string[] = [\n '',\n '╔══════════════════════════════════════════════════════════════╗',\n '║ ⚠ INVECT — PLUGIN TABLES MISSING ⚠ ║',\n '╚══════════════════════════════════════════════════════════════╝',\n '',\n `${totalMissing} table(s) required by ${pluginsWithMissing.length} plugin(s) are missing from the database:`,\n '',\n ];\n\n for (const plugin of pluginsWithMissing) {\n lines.push(\n ` Plugin: ${plugin.pluginName} (${plugin.pluginId})`,\n ` Missing tables: ${plugin.missingTables.join(', ')}`,\n );\n\n if (plugin.setupInstructions) {\n lines.push(` Fix: ${plugin.setupInstructions}`);\n }\n\n lines.push('');\n }\n\n // Check if any plugin provided custom instructions\n const hasCustomInstructions = pluginsWithMissing.some((p) => p.setupInstructions);\n\n if (!hasCustomInstructions) {\n // Generic fix instructions — point to the CLI\n lines.push(\n 'To fix this, run:',\n '',\n ' npx invect generate # generate schema files (core + plugins)',\n ' npx drizzle-kit push # push schema to the database',\n '',\n 'The Invect CLI reads your invect.config.ts, discovers all plugins',\n 'and their required tables, and generates the complete schema.',\n );\n }\n\n lines.push(\n '',\n 'If a plugin defines a schema, `npx invect generate` will include it',\n 'automatically. For plugins with externally-managed tables, see the',\n \"plugin's README for additional schema setup instructions.\",\n '',\n );\n\n const message = lines.join('\\n');\n this.logger.error(message);\n\n throw new DatabaseError(\n `Database is missing ${totalMissing} table(s) required by plugin(s): ` +\n pluginsWithMissing\n .map((p) => `${p.pluginName} (${p.missingTables.join(', ')})`)\n .join('; ') +\n `. Push the schema before starting the server. See logs above for instructions.`,\n {\n pluginsWithMissing: pluginsWithMissing.map((p) => ({\n pluginId: p.pluginId,\n missingTables: p.missingTables,\n })),\n },\n );\n }\n\n /**\n * Get a set of table names from the database.\n */\n private async listTableNames(connection: DatabaseConnection): Promise<Set<string>> {\n const names = new Set<string>();\n\n switch (connection.type) {\n case 'sqlite': {\n const db = connection.db as unknown as SqliteDbLike;\n // Note: In SQL LIKE, '_' is a single-char wildcard. We must escape it\n // with ESCAPE '\\' so 'sqlite\\_%' matches literal 'sqlite_*' and\n // '\\\\_\\\\_%' matches literal names starting with '__'.\n const query = `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite\\\\_%' ESCAPE '\\\\' AND name NOT LIKE '\\\\_\\\\_%' ESCAPE '\\\\'`;\n const rows = db.$client.prepare(query).all() as Array<{ name: string }>;\n for (const row of rows) {\n names.add(row.name);\n }\n break;\n }\n case 'postgresql': {\n const db = connection.db as unknown as PostgreSqlDbLike;\n const rows = (await db.$client`\n SELECT table_name FROM information_schema.tables\n WHERE table_schema = 'public' AND table_type = 'BASE TABLE'\n `) as Array<{ table_name: string }>;\n for (const row of rows) {\n names.add(row.table_name);\n }\n break;\n }\n case 'mysql': {\n const db = connection.db as unknown as MysqlDbLike;\n const [rows] = await db.$client.execute(\n `SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'`,\n );\n for (const row of rows as Array<Record<string, string>>) {\n names.add(row['TABLE_NAME']);\n }\n break;\n }\n }\n\n return names;\n }\n\n /**\n * Log a helpful error when the database connection itself fails.\n */\n private logConnectionError(error: unknown): void {\n const msg = error instanceof Error ? error.message : String(error);\n const dbType = this.hostDbConfig.type;\n const connStr = this.hostDbConfig.connectionString;\n\n const lines: string[] = [\n '',\n '╔══════════════════════════════════════════════════════════════╗',\n '║ ⚠ INVECT — DATABASE CONNECTION FAILED ⚠ ║',\n '╚══════════════════════════════════════════════════════════════╝',\n '',\n `Database type: ${dbType}`,\n `Connection string: ${this.redactConnectionString(connStr)}`,\n `Error: ${msg}`,\n '',\n ];\n\n if (dbType === 'sqlite') {\n if (msg.includes('SQLITE_CANTOPEN') || msg.includes('unable to open')) {\n lines.push(\n 'The SQLite database file could not be opened.',\n 'Check that the path is correct and the directory exists.',\n '',\n ` Configured path: ${connStr}`,\n );\n } else {\n lines.push(\n 'Could not connect to the SQLite database.',\n 'Make sure the connection string in your config is valid.',\n '',\n 'Common formats:',\n ' file:./dev.db (relative path)',\n ' file:/absolute/path.db (absolute path)',\n );\n }\n } else if (dbType === 'postgresql') {\n if (msg.includes('ECONNREFUSED') || msg.includes('connect')) {\n lines.push(\n 'Could not reach the PostgreSQL server.',\n 'Make sure PostgreSQL is running and the connection string is correct.',\n '',\n 'Common causes:',\n ' • PostgreSQL is not running (start with: pg_ctl start)',\n ' • Wrong host/port in connection string',\n ' • Firewall blocking the connection',\n );\n } else if (msg.includes('authentication') || msg.includes('password')) {\n lines.push(\n 'PostgreSQL authentication failed.',\n 'Check that the username and password in your connection string are correct.',\n );\n } else if (msg.includes('does not exist')) {\n lines.push(\n 'The PostgreSQL database does not exist.',\n 'Create it with: createdb <database_name>',\n );\n } else {\n lines.push('Could not connect to PostgreSQL. Verify your DATABASE_URL is correct.');\n }\n } else if (dbType === 'mysql') {\n if (msg.includes('ECONNREFUSED') || msg.includes('connect')) {\n lines.push(\n 'Could not reach the MySQL server.',\n 'Make sure MySQL is running and the connection string is correct.',\n );\n } else {\n lines.push('Could not connect to MySQL. Verify your connection string.');\n }\n }\n\n lines.push('');\n this.logger.error(lines.join('\\n'));\n }\n\n /**\n * Log a helpful error when the connectivity probe (SELECT 1) fails.\n */\n private logConnectivityError(error: unknown): void {\n const msg = error instanceof Error ? error.message : String(error);\n\n const lines: string[] = [\n '',\n '╔══════════════════════════════════════════════════════════════╗',\n '║ ⚠ INVECT — DATABASE CONNECTIVITY CHECK FAILED ⚠ ║',\n '╚══════════════════════════════════════════════════════════════╝',\n '',\n 'A connection was established but a simple SELECT query failed.',\n `Error: ${msg}`,\n '',\n 'This may indicate:',\n ' • The database server dropped the connection',\n ' • Insufficient permissions for the database user',\n ' • The database file is corrupted (SQLite)',\n '',\n ];\n\n this.logger.error(lines.join('\\n'));\n }\n\n /**\n * Redact credentials from a connection string for safe logging.\n */\n private redactConnectionString(connStr: string): string {\n // Hide passwords in postgres/mysql URLs: postgres://user:PASS@host → postgres://user:***@host\n return connStr.replace(/:([^/:@]+)@/, ':***@');\n }\n}\n\n/**\n * Database Models Factory - Creates all model instances with shared connection and logger\n */\nclass Database {\n public readonly flows: FlowsModel;\n public readonly flowVersions: FlowVersionsModel;\n public readonly flowRuns: FlowRunsModel;\n public readonly executionTraces: NodeExecutionsModel;\n public readonly batchJobs: BatchJobsModel;\n public readonly agentToolExecutions: AgentToolExecutionsModel;\n public readonly flowTriggers: FlowTriggersModel;\n public readonly chatMessages: ChatMessagesModel;\n\n constructor(_connection: DatabaseConnection, logger: Logger, adapter: InvectAdapter) {\n this.flows = new FlowsModel(adapter, logger);\n this.flowVersions = new FlowVersionsModel(adapter, logger);\n this.flowRuns = new FlowRunsModel(adapter, logger);\n this.executionTraces = new NodeExecutionsModel(adapter, logger);\n this.batchJobs = new BatchJobsModel(adapter, logger);\n this.agentToolExecutions = new AgentToolExecutionsModel(adapter, logger);\n this.flowTriggers = new FlowTriggersModel(adapter, logger);\n this.chatMessages = new ChatMessagesModel(adapter, logger);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database.service.cjs","names":["DatabaseError","DatabaseConnectionFactory","createAdapterFromConnection","verifySchema","CORE_SCHEMA","FlowsModel","FlowVersionsModel","FlowRunsModel","NodeExecutionsModel","BatchJobsModel","AgentToolExecutionsModel","FlowTriggersModel","ChatMessagesModel"],"sources":["../../../src/services/database/database.service.ts"],"sourcesContent":["// Framework-agnostic Database Service for Invect core\n\nimport { DatabaseConnectionFactory, type DatabaseConnection } from '../../database/connection';\nimport { verifySchema, type SchemaVerificationOptions } from '../../database/schema-verification';\nimport { CORE_SCHEMA } from '../../database/core-schema';\nimport { DatabaseError } from 'src/types/common/errors.types';\nimport { FlowRunsModel } from '../flow-runs/flow-runs.model';\nimport { FlowsModel } from '../flows/flows.model';\nimport { BatchJobsModel } from '../batch-jobs/batch-jobs.model';\nimport { FlowVersionsModel } from '../flow-versions/flow-versions.model';\nimport { NodeExecutionsModel } from '../node-executions/node-executions.model';\nimport { AgentToolExecutionsModel } from '../agent-tool-executions/agent-tool-executions.model';\nimport { FlowTriggersModel } from '../triggers/flow-triggers.model';\nimport { ChatMessagesModel } from '../chat/chat-messages.model';\nimport { InvectDatabaseConfig, Logger } from 'src/types/schemas';\nimport type { InvectPlugin } from 'src/types/plugin.types';\nimport type { InvectAdapter } from '../../database/adapter';\nimport { createAdapterFromConnection } from '../../database/adapters/connection-bridge';\n\ntype PostgreSqlClientLike = {\n <T = Record<string, unknown>>(query: string): Promise<T[]>;\n (strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;\n};\n\ntype SqliteClientLike = {\n prepare(sql: string): {\n all(...params: unknown[]): Record<string, unknown>[];\n run(...params: unknown[]): { changes: number };\n get(...params: unknown[]): Record<string, unknown> | undefined;\n };\n exec(sql: string): void;\n};\n\ntype MysqlClientLike = {\n execute<T = Record<string, unknown>>(\n query: string,\n params?: unknown[],\n ): Promise<[T[] | unknown, unknown]>;\n};\n\ntype PostgreSqlDbLike = {\n $client: PostgreSqlClientLike;\n};\n\ntype SqliteDbLike = {\n $client: SqliteClientLike;\n};\n\ntype MysqlDbLike = {\n $client: MysqlClientLike;\n};\n\n/**\n * Describes tables that a plugin needs checked at startup.\n */\nexport interface PluginTableRequirement {\n pluginId: string;\n pluginName: string;\n tables: string[];\n setupInstructions?: string;\n}\n\n/**\n * Core database service implementation\n */\nexport class DatabaseService {\n private connection: DatabaseConnection | null = null;\n private _database: Database | null = null;\n private _adapter: InvectAdapter | null = null;\n private schemaVerificationOptions?: SchemaVerificationOptions;\n private pluginTableRequirements: PluginTableRequirement[] = [];\n\n constructor(\n private readonly hostDbConfig: InvectDatabaseConfig,\n private readonly logger: Logger = console,\n schemaVerification?: SchemaVerificationOptions,\n plugins?: InvectPlugin[],\n ) {\n this.schemaVerificationOptions = schemaVerification;\n this.pluginTableRequirements = DatabaseService.extractPluginTableRequirements(plugins ?? []);\n }\n\n /**\n * Get the Database instance with all model classes\n */\n get database(): Database {\n if (!this._database) {\n throw new DatabaseError('Database not initialized - call initialize() first');\n }\n return this._database;\n }\n\n /**\n * Get the InvectAdapter instance for direct adapter access\n */\n get adapter(): InvectAdapter {\n if (!this._adapter) {\n throw new DatabaseError('Database not initialized - call initialize() first');\n }\n return this._adapter;\n }\n\n /**\n * Direct access to flows model\n */\n get flows() {\n return this.database.flows;\n }\n\n /**\n * Direct access to flow versions model\n */\n get flowVersions() {\n return this.database.flowVersions;\n }\n\n /**\n * Direct access to flow executions model\n */\n get flowRuns() {\n return this.database.flowRuns;\n }\n\n /**\n * Direct access to execution traces model\n */\n get nodeExecutions() {\n return this.database.executionTraces;\n }\n\n /**\n * Direct access to batch jobs model\n */\n get batchJobs() {\n return this.database.batchJobs;\n }\n\n /**\n * Direct access to agent tool executions model\n */\n get agentToolExecutions() {\n return this.database.agentToolExecutions;\n }\n\n /**\n * Direct access to flow triggers model\n */\n get flowTriggers() {\n return this.database.flowTriggers;\n }\n\n /**\n * Direct access to chat messages model\n */\n get chatMessages() {\n return this.database.chatMessages;\n }\n\n /**\n * Initialize the database connection and models\n */\n async initialize(): Promise<void> {\n if (this.connection) {\n this.logger.debug('Database connection already initialized');\n return;\n }\n\n // --- Step 1: Establish the database connection ---\n try {\n this.connection = await DatabaseConnectionFactory.createHostDBConnection(\n this.hostDbConfig,\n this.logger,\n );\n } catch (error) {\n this.logConnectionError(error);\n throw new DatabaseError(\n `Failed to connect to the database: ${error instanceof Error ? error.message : error}`,\n { error, originalStack: error instanceof Error ? error.stack : undefined },\n );\n }\n\n // --- Step 2: Verify connectivity with a simple query ---\n try {\n await this.runConnectivityCheck(this.connection);\n } catch (error) {\n this.logConnectivityError(error);\n throw new DatabaseError(\n `Database connectivity check failed: ${error instanceof Error ? error.message : error}`,\n { error },\n );\n }\n\n // --- Step 3: Check that core Invect tables exist ---\n // This always runs (regardless of schemaVerification config) to catch\n // the common case of a fresh database that hasn't had migrations applied.\n await this.runCoreTableCheck(this.connection);\n\n // --- Step 3b: Check that plugin-required tables exist ---\n // Each plugin can declare requiredTables to get a clear error at startup\n // instead of a cryptic runtime crash when a table is missing.\n await this.runPluginTableChecks(this.connection);\n\n // --- Step 4: Initialize the database models ---\n this._adapter = createAdapterFromConnection(this.connection);\n this._database = new Database(this.connection, this.logger, this._adapter);\n\n // --- Step 5: Run detailed schema verification (opt-in) ---\n // This checks columns, not just tables. Controlled by config.schemaVerification.\n if (this.schemaVerificationOptions) {\n await verifySchema(this.connection, this.logger, this.schemaVerificationOptions);\n }\n\n this.logger.info('Database service initialized successfully');\n }\n\n /**\n * Get the database connection for sharing with other services\n */\n getConnection(): DatabaseConnection {\n if (!this.connection) {\n throw new DatabaseError('Database not initialized - call initialize() first');\n }\n return this.connection;\n }\n\n /**\n * Execute a SQL query on a provided queryDatabase\n */\n async executeQuery(\n query: string,\n queryDBConfig: InvectDatabaseConfig,\n ): Promise<Record<string, unknown>[]> {\n try {\n this.logger.debug('Executing query on external database', {\n query,\n dbType: queryDBConfig.type,\n dbId: queryDBConfig.id,\n });\n\n // Create or get existing query database connection\n const queryConnection = await DatabaseConnectionFactory.createQueryDbConnection(\n queryDBConfig,\n this.logger,\n );\n\n // Execute the query based on database type and return results\n let result: Record<string, unknown>[] = [];\n\n switch (queryDBConfig.type) {\n case 'postgresql': {\n // PostgreSQL query execution using postgres.js client directly\n const client = (queryConnection.db as unknown as PostgreSqlDbLike).$client;\n result = await client<Record<string, unknown>>(query);\n break;\n }\n case 'sqlite': {\n // SQLite query execution using better-sqlite3 synchronous API\n const sqliteClient = (queryConnection.db as unknown as SqliteDbLike).$client;\n result = sqliteClient.prepare(query).all() as Record<string, unknown>[];\n break;\n }\n case 'mysql': {\n // MySQL query execution using mysql2 client directly\n const client = (queryConnection.db as unknown as MysqlDbLike).$client;\n const [rows] = await client.execute<Record<string, unknown>>(query);\n result = Array.isArray(rows) ? rows : [];\n break;\n }\n default:\n throw new DatabaseError(`Unsupported database type: ${queryDBConfig.type}`);\n }\n\n this.logger.debug('Query executed successfully', {\n rowCount: Array.isArray(result) ? result.length : 'unknown',\n dbType: queryDBConfig.type,\n dbId: queryDBConfig.id,\n });\n\n return result;\n } catch (error) {\n this.logger.error('Failed to execute query on external database', {\n error: error instanceof Error ? error.message : error,\n query,\n dbType: queryDBConfig.type,\n dbId: queryDBConfig.id,\n stack: error instanceof Error ? error.stack : undefined,\n });\n\n throw new DatabaseError(\n `Query execution failed on ${queryDBConfig.type} database (${queryDBConfig.id}): ${\n error instanceof Error ? error.message : error\n }`,\n {\n error,\n query,\n dbConfig: queryDBConfig,\n originalStack: error instanceof Error ? error.stack : undefined,\n },\n );\n }\n }\n\n /**\n * Health check method\n */\n async healthCheck(): Promise<void> {\n this.logger.debug('Performing database health check');\n\n if (!this.connection) {\n throw new DatabaseError('Database not initialized - call initialize() first');\n }\n\n try {\n // Simple connectivity test using host database connection\n switch (this.connection.type) {\n case 'postgresql': {\n const client = (this.connection.db as unknown as PostgreSqlDbLike).$client;\n await client`SELECT 1 as health`;\n break;\n }\n case 'sqlite': {\n const client = (this.connection.db as unknown as SqliteDbLike).$client;\n client.prepare('SELECT 1 as health').get();\n break;\n }\n case 'mysql': {\n const client = (this.connection.db as unknown as MysqlDbLike).$client;\n await client.execute('SELECT 1 as health');\n break;\n }\n default:\n throw new DatabaseError('Unsupported database type for health check');\n }\n\n this.logger.debug('Database health check passed');\n } catch (error) {\n this.logger.error('Database health check failed', error);\n throw new DatabaseError('Database health check failed', { error });\n }\n }\n\n /**\n * Close the database connection\n */\n async close(): Promise<void> {\n if (this.connection) {\n this.logger.debug('Closing database connection');\n this.connection = null;\n this._database = null;\n this.logger.info('Database connection closed');\n }\n }\n\n // ===========================================================================\n // Plugin table requirement extraction\n // ===========================================================================\n\n /**\n * Extract table requirements from plugin declarations.\n *\n * A plugin can declare required tables via:\n * 1. `requiredTables: string[]` — explicit list (preferred)\n * 2. `schema: { tableName: ... }` — inferred from abstract schema definitions\n *\n * If both are present, `requiredTables` takes precedence.\n */\n static extractPluginTableRequirements(plugins: InvectPlugin[]): PluginTableRequirement[] {\n const requirements: PluginTableRequirement[] = [];\n\n for (const plugin of plugins) {\n let tables: string[] = [];\n\n if (plugin.requiredTables && plugin.requiredTables.length > 0) {\n // Explicit declaration takes priority\n tables = [...plugin.requiredTables];\n } else if (plugin.schema) {\n // Infer from abstract schema — each entry's tableName (or snake_case key)\n for (const [key, def] of Object.entries(plugin.schema)) {\n const tableName = (def as { tableName?: string }).tableName ?? key;\n const disabled = (def as { disableMigration?: boolean }).disableMigration;\n if (!disabled) {\n tables.push(tableName);\n }\n }\n }\n\n if (tables.length > 0) {\n requirements.push({\n pluginId: plugin.id,\n pluginName: plugin.name ?? plugin.id,\n tables,\n setupInstructions: plugin.setupInstructions,\n });\n }\n }\n\n return requirements;\n }\n\n // ===========================================================================\n // Startup check helpers\n // ===========================================================================\n\n /**\n * Run a simple `SELECT 1` query to verify the database is reachable.\n */\n private async runConnectivityCheck(connection: DatabaseConnection): Promise<void> {\n switch (connection.type) {\n case 'postgresql': {\n const client = (connection.db as unknown as PostgreSqlDbLike).$client;\n await client`SELECT 1 as health`;\n break;\n }\n case 'sqlite': {\n const client = (connection.db as unknown as SqliteDbLike).$client;\n client.prepare('SELECT 1 as health').get();\n break;\n }\n case 'mysql': {\n const client = (connection.db as unknown as MysqlDbLike).$client;\n await client.execute('SELECT 1 as health');\n break;\n }\n }\n }\n\n /**\n * Check that the essential Invect tables exist in the database.\n * This catches the most common developer mistake: running the app\n * before applying the database schema.\n *\n * Only checks for table *existence*, not column correctness (that's\n * what the opt-in `schemaVerification` does).\n */\n private async runCoreTableCheck(connection: DatabaseConnection): Promise<void> {\n // Collect the expected core table names from the abstract schema\n const expectedTables: string[] = [];\n for (const def of Object.values(CORE_SCHEMA)) {\n const tableDef = def as { tableName?: string; disableMigration?: boolean };\n if (tableDef.disableMigration) {\n continue;\n }\n if (tableDef.tableName) {\n expectedTables.push(tableDef.tableName);\n }\n }\n\n // Get actual table names from the database\n let actualTableNames: Set<string>;\n try {\n actualTableNames = await this.listTableNames(connection);\n } catch (error) {\n // If we can't even list tables, the database is probably unreachable\n // or misconfigured — the connectivity check should have caught this,\n // but log and proceed gracefully.\n this.logger.warn('Could not introspect database tables. Skipping startup table check.', {\n error: error instanceof Error ? error.message : error,\n });\n return;\n }\n\n const missingTables = expectedTables.filter((t) => !actualTableNames.has(t));\n\n if (missingTables.length === 0) {\n this.logger.debug('Core table check passed', {\n tablesFound: expectedTables.length,\n });\n return;\n }\n\n // --- Build a helpful error message ---\n const allMissing = missingTables.length === expectedTables.length;\n\n const lines: string[] = [\n '',\n '╔══════════════════════════════════════════════════════════════╗',\n '║ ⚠ INVECT — DATABASE NOT READY ⚠ ║',\n '╚══════════════════════════════════════════════════════════════╝',\n '',\n ];\n\n if (allMissing) {\n lines.push(\n 'Your database exists but has no Invect tables.',\n \"This usually means you haven't pushed the schema yet.\",\n );\n } else {\n lines.push(\n `Your database is missing ${missingTables.length} of ${expectedTables.length} required Invect tables:`,\n ` Missing: ${missingTables.join(', ')}`,\n '',\n 'This usually means your schema is out of date.',\n );\n }\n\n lines.push('');\n\n // Fix instructions — point users to the Invect CLI\n lines.push(\n 'To fix this, run:',\n '',\n ' npx invect-cli generate # generate schema files (core + plugins)',\n ' npx drizzle-kit push # push schema to the database',\n '',\n 'Or if you use migrations:',\n '',\n ' npx invect-cli generate # generate schema files',\n ' npx drizzle-kit generate',\n ' npx invect-cli migrate # apply migrations',\n '',\n 'The Invect CLI reads your invect.config.ts to discover installed',\n 'plugins and generates the correct schema for all of them.',\n '',\n );\n\n const message = lines.join('\\n');\n this.logger.error(message);\n\n throw new DatabaseError(\n `Database is missing ${allMissing ? 'all' : missingTables.length} required Invect table(s). ` +\n `Run schema migrations before starting the server. See logs above for instructions.`,\n { missingTables },\n );\n }\n\n /**\n * Check that tables required by plugins exist in the database.\n *\n * Each plugin can declare `requiredTables` (explicit list) or have them\n * inferred from its `schema` definition. This method checks all of them\n * in a single pass and produces a clear, attributed error message so the\n * developer knows exactly which plugin needs which tables.\n */\n private async runPluginTableChecks(connection: DatabaseConnection): Promise<void> {\n if (this.pluginTableRequirements.length === 0) {\n return; // No plugins declared required tables\n }\n\n // Get actual table names from the database\n let actualTableNames: Set<string>;\n try {\n actualTableNames = await this.listTableNames(connection);\n } catch {\n // If introspection fails, skip gracefully (core check already warned)\n return;\n }\n\n // Collect all missing tables grouped by plugin\n const pluginsWithMissing: Array<{\n pluginId: string;\n pluginName: string;\n missingTables: string[];\n setupInstructions?: string;\n }> = [];\n\n for (const req of this.pluginTableRequirements) {\n const missing = req.tables.filter((t) => !actualTableNames.has(t));\n if (missing.length > 0) {\n pluginsWithMissing.push({\n pluginId: req.pluginId,\n pluginName: req.pluginName,\n missingTables: missing,\n setupInstructions: req.setupInstructions,\n });\n }\n }\n\n if (pluginsWithMissing.length === 0) {\n const totalTables = this.pluginTableRequirements.reduce((sum, r) => sum + r.tables.length, 0);\n this.logger.debug('Plugin table check passed', {\n plugins: this.pluginTableRequirements.length,\n tablesChecked: totalTables,\n });\n return;\n }\n\n // --- Build a helpful, plugin-attributed error message ---\n const totalMissing = pluginsWithMissing.reduce((sum, p) => sum + p.missingTables.length, 0);\n\n const lines: string[] = [\n '',\n '╔══════════════════════════════════════════════════════════════╗',\n '║ ⚠ INVECT — PLUGIN TABLES MISSING ⚠ ║',\n '╚══════════════════════════════════════════════════════════════╝',\n '',\n `${totalMissing} table(s) required by ${pluginsWithMissing.length} plugin(s) are missing from the database:`,\n '',\n ];\n\n for (const plugin of pluginsWithMissing) {\n lines.push(\n ` Plugin: ${plugin.pluginName} (${plugin.pluginId})`,\n ` Missing tables: ${plugin.missingTables.join(', ')}`,\n );\n\n if (plugin.setupInstructions) {\n lines.push(` Fix: ${plugin.setupInstructions}`);\n }\n\n lines.push('');\n }\n\n // Check if any plugin provided custom instructions\n const hasCustomInstructions = pluginsWithMissing.some((p) => p.setupInstructions);\n\n if (!hasCustomInstructions) {\n // Generic fix instructions — point to the CLI\n lines.push(\n 'To fix this, run:',\n '',\n ' npx invect-cli generate # generate schema files (core + plugins)',\n ' npx drizzle-kit push # push schema to the database',\n '',\n 'The Invect CLI reads your invect.config.ts, discovers all plugins',\n 'and their required tables, and generates the complete schema.',\n );\n }\n\n lines.push(\n '',\n 'If a plugin defines a schema, `npx invect-cli generate` will include it',\n 'automatically. For plugins with externally-managed tables, see the',\n \"plugin's README for additional schema setup instructions.\",\n '',\n );\n\n const message = lines.join('\\n');\n this.logger.error(message);\n\n throw new DatabaseError(\n `Database is missing ${totalMissing} table(s) required by plugin(s): ` +\n pluginsWithMissing\n .map((p) => `${p.pluginName} (${p.missingTables.join(', ')})`)\n .join('; ') +\n `. Push the schema before starting the server. See logs above for instructions.`,\n {\n pluginsWithMissing: pluginsWithMissing.map((p) => ({\n pluginId: p.pluginId,\n missingTables: p.missingTables,\n })),\n },\n );\n }\n\n /**\n * Get a set of table names from the database.\n */\n private async listTableNames(connection: DatabaseConnection): Promise<Set<string>> {\n const names = new Set<string>();\n\n switch (connection.type) {\n case 'sqlite': {\n const db = connection.db as unknown as SqliteDbLike;\n // Note: In SQL LIKE, '_' is a single-char wildcard. We must escape it\n // with ESCAPE '\\' so 'sqlite\\_%' matches literal 'sqlite_*' and\n // '\\\\_\\\\_%' matches literal names starting with '__'.\n const query = `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite\\\\_%' ESCAPE '\\\\' AND name NOT LIKE '\\\\_\\\\_%' ESCAPE '\\\\'`;\n const rows = db.$client.prepare(query).all() as Array<{ name: string }>;\n for (const row of rows) {\n names.add(row.name);\n }\n break;\n }\n case 'postgresql': {\n const db = connection.db as unknown as PostgreSqlDbLike;\n const rows = (await db.$client`\n SELECT table_name FROM information_schema.tables\n WHERE table_schema = 'public' AND table_type = 'BASE TABLE'\n `) as Array<{ table_name: string }>;\n for (const row of rows) {\n names.add(row.table_name);\n }\n break;\n }\n case 'mysql': {\n const db = connection.db as unknown as MysqlDbLike;\n const [rows] = await db.$client.execute(\n `SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'`,\n );\n for (const row of rows as Array<Record<string, string>>) {\n names.add(row['TABLE_NAME']);\n }\n break;\n }\n }\n\n return names;\n }\n\n /**\n * Log a helpful error when the database connection itself fails.\n */\n private logConnectionError(error: unknown): void {\n const msg = error instanceof Error ? error.message : String(error);\n const dbType = this.hostDbConfig.type;\n const connStr = this.hostDbConfig.connectionString;\n\n const lines: string[] = [\n '',\n '╔══════════════════════════════════════════════════════════════╗',\n '║ ⚠ INVECT — DATABASE CONNECTION FAILED ⚠ ║',\n '╚══════════════════════════════════════════════════════════════╝',\n '',\n `Database type: ${dbType}`,\n `Connection string: ${this.redactConnectionString(connStr)}`,\n `Error: ${msg}`,\n '',\n ];\n\n if (dbType === 'sqlite') {\n if (msg.includes('SQLITE_CANTOPEN') || msg.includes('unable to open')) {\n lines.push(\n 'The SQLite database file could not be opened.',\n 'Check that the path is correct and the directory exists.',\n '',\n ` Configured path: ${connStr}`,\n );\n } else {\n lines.push(\n 'Could not connect to the SQLite database.',\n 'Make sure the connection string in your config is valid.',\n '',\n 'Common formats:',\n ' file:./dev.db (relative path)',\n ' file:/absolute/path.db (absolute path)',\n );\n }\n } else if (dbType === 'postgresql') {\n if (msg.includes('ECONNREFUSED') || msg.includes('connect')) {\n lines.push(\n 'Could not reach the PostgreSQL server.',\n 'Make sure PostgreSQL is running and the connection string is correct.',\n '',\n 'Common causes:',\n ' • PostgreSQL is not running (start with: pg_ctl start)',\n ' • Wrong host/port in connection string',\n ' • Firewall blocking the connection',\n );\n } else if (msg.includes('authentication') || msg.includes('password')) {\n lines.push(\n 'PostgreSQL authentication failed.',\n 'Check that the username and password in your connection string are correct.',\n );\n } else if (msg.includes('does not exist')) {\n lines.push(\n 'The PostgreSQL database does not exist.',\n 'Create it with: createdb <database_name>',\n );\n } else {\n lines.push('Could not connect to PostgreSQL. Verify your DATABASE_URL is correct.');\n }\n } else if (dbType === 'mysql') {\n if (msg.includes('ECONNREFUSED') || msg.includes('connect')) {\n lines.push(\n 'Could not reach the MySQL server.',\n 'Make sure MySQL is running and the connection string is correct.',\n );\n } else {\n lines.push('Could not connect to MySQL. Verify your connection string.');\n }\n }\n\n lines.push('');\n this.logger.error(lines.join('\\n'));\n }\n\n /**\n * Log a helpful error when the connectivity probe (SELECT 1) fails.\n */\n private logConnectivityError(error: unknown): void {\n const msg = error instanceof Error ? error.message : String(error);\n\n const lines: string[] = [\n '',\n '╔══════════════════════════════════════════════════════════════╗',\n '║ ⚠ INVECT — DATABASE CONNECTIVITY CHECK FAILED ⚠ ║',\n '╚══════════════════════════════════════════════════════════════╝',\n '',\n 'A connection was established but a simple SELECT query failed.',\n `Error: ${msg}`,\n '',\n 'This may indicate:',\n ' • The database server dropped the connection',\n ' • Insufficient permissions for the database user',\n ' • The database file is corrupted (SQLite)',\n '',\n ];\n\n this.logger.error(lines.join('\\n'));\n }\n\n /**\n * Redact credentials from a connection string for safe logging.\n */\n private redactConnectionString(connStr: string): string {\n // Hide passwords in postgres/mysql URLs: postgres://user:PASS@host → postgres://user:***@host\n return connStr.replace(/:([^/:@]+)@/, ':***@');\n }\n}\n\n/**\n * Database Models Factory - Creates all model instances with shared connection and logger\n */\nclass Database {\n public readonly flows: FlowsModel;\n public readonly flowVersions: FlowVersionsModel;\n public readonly flowRuns: FlowRunsModel;\n public readonly executionTraces: NodeExecutionsModel;\n public readonly batchJobs: BatchJobsModel;\n public readonly agentToolExecutions: AgentToolExecutionsModel;\n public readonly flowTriggers: FlowTriggersModel;\n public readonly chatMessages: ChatMessagesModel;\n\n constructor(_connection: DatabaseConnection, logger: Logger, adapter: InvectAdapter) {\n this.flows = new FlowsModel(adapter, logger);\n this.flowVersions = new FlowVersionsModel(adapter, logger);\n this.flowRuns = new FlowRunsModel(adapter, logger);\n this.executionTraces = new NodeExecutionsModel(adapter, logger);\n this.batchJobs = new BatchJobsModel(adapter, logger);\n this.agentToolExecutions = new AgentToolExecutionsModel(adapter, logger);\n this.flowTriggers = new FlowTriggersModel(adapter, logger);\n this.chatMessages = new ChatMessagesModel(adapter, logger);\n }\n}\n"],"mappings":"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"}
|
|
@@ -293,7 +293,7 @@ var DatabaseService = class DatabaseService {
|
|
|
293
293
|
if (allMissing) lines.push("Your database exists but has no Invect tables.", "This usually means you haven't pushed the schema yet.");
|
|
294
294
|
else lines.push(`Your database is missing ${missingTables.length} of ${expectedTables.length} required Invect tables:`, ` Missing: ${missingTables.join(", ")}`, "", "This usually means your schema is out of date.");
|
|
295
295
|
lines.push("");
|
|
296
|
-
lines.push("To fix this, run:", "", " npx invect generate # generate schema files (core + plugins)", " npx drizzle-kit push # push schema to the database", "", "Or if you use migrations:", "", " npx invect generate # generate schema files", " npx drizzle-kit generate", " npx invect migrate # apply migrations", "", "The Invect CLI reads your invect.config.ts to discover installed", "plugins and generates the correct schema for all of them.", "");
|
|
296
|
+
lines.push("To fix this, run:", "", " npx invect-cli generate # generate schema files (core + plugins)", " npx drizzle-kit push # push schema to the database", "", "Or if you use migrations:", "", " npx invect-cli generate # generate schema files", " npx drizzle-kit generate", " npx invect-cli migrate # apply migrations", "", "The Invect CLI reads your invect.config.ts to discover installed", "plugins and generates the correct schema for all of them.", "");
|
|
297
297
|
const message = lines.join("\n");
|
|
298
298
|
this.logger.error(message);
|
|
299
299
|
throw new DatabaseError(`Database is missing ${allMissing ? "all" : missingTables.length} required Invect table(s). Run schema migrations before starting the server. See logs above for instructions.`, { missingTables });
|
|
@@ -347,8 +347,8 @@ var DatabaseService = class DatabaseService {
|
|
|
347
347
|
if (plugin.setupInstructions) lines.push(` Fix: ${plugin.setupInstructions}`);
|
|
348
348
|
lines.push("");
|
|
349
349
|
}
|
|
350
|
-
if (!pluginsWithMissing.some((p) => p.setupInstructions)) lines.push("To fix this, run:", "", " npx invect generate # generate schema files (core + plugins)", " npx drizzle-kit push # push schema to the database", "", "The Invect CLI reads your invect.config.ts, discovers all plugins", "and their required tables, and generates the complete schema.");
|
|
351
|
-
lines.push("", "If a plugin defines a schema, `npx invect generate` will include it", "automatically. For plugins with externally-managed tables, see the", "plugin's README for additional schema setup instructions.", "");
|
|
350
|
+
if (!pluginsWithMissing.some((p) => p.setupInstructions)) lines.push("To fix this, run:", "", " npx invect-cli generate # generate schema files (core + plugins)", " npx drizzle-kit push # push schema to the database", "", "The Invect CLI reads your invect.config.ts, discovers all plugins", "and their required tables, and generates the complete schema.");
|
|
351
|
+
lines.push("", "If a plugin defines a schema, `npx invect-cli generate` will include it", "automatically. For plugins with externally-managed tables, see the", "plugin's README for additional schema setup instructions.", "");
|
|
352
352
|
const message = lines.join("\n");
|
|
353
353
|
this.logger.error(message);
|
|
354
354
|
throw new DatabaseError(`Database is missing ${totalMissing} table(s) required by plugin(s): ` + pluginsWithMissing.map((p) => `${p.pluginName} (${p.missingTables.join(", ")})`).join("; ") + `. Push the schema before starting the server. See logs above for instructions.`, { pluginsWithMissing: pluginsWithMissing.map((p) => ({
|