@qwickapps/server 1.7.2 → 1.8.0
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/CHANGELOG.md +33 -0
- package/dist/src/core/control-panel.js +5 -5
- package/dist/src/core/control-panel.js.map +1 -1
- package/dist/src/core/gateway.d.ts.map +1 -1
- package/dist/src/core/gateway.js +117 -15
- package/dist/src/core/gateway.js.map +1 -1
- package/dist/src/core/plugin-registry.d.ts +70 -0
- package/dist/src/core/plugin-registry.d.ts.map +1 -1
- package/dist/src/core/plugin-registry.js +94 -0
- package/dist/src/core/plugin-registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.js +53 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.js.map +1 -1
- package/dist/src/plugins/api-keys/index.d.ts +1 -1
- package/dist/src/plugins/api-keys/index.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/index.js.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.js +83 -65
- package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
- package/dist/src/plugins/api-keys/types.d.ts +13 -1
- package/dist/src/plugins/api-keys/types.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/types.js.map +1 -1
- package/dist/src/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/src/plugins/diagnostics-plugin.js +73 -0
- package/dist/src/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/src/plugins/index.d.ts +1 -1
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedExecutor.d.ts +2 -0
- package/dist/src/plugins/maintenance/SeedExecutor.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedExecutor.js +6 -2
- package/dist/src/plugins/maintenance/SeedExecutor.js.map +1 -1
- package/dist/src/plugins/maintenance/SeedList.d.ts +2 -2
- package/dist/src/plugins/maintenance/SeedList.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedList.js +39 -14
- package/dist/src/plugins/maintenance/SeedList.js.map +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.d.ts +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.js +9 -5
- package/dist/src/plugins/maintenance/SeedManagementPage.js.map +1 -1
- package/dist/src/plugins/maintenance/seed-executor.d.ts +6 -4
- package/dist/src/plugins/maintenance/seed-executor.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/seed-executor.js +53 -17
- package/dist/src/plugins/maintenance/seed-executor.js.map +1 -1
- package/dist/src/plugins/maintenance-plugin.d.ts +24 -0
- package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
- package/dist/src/plugins/maintenance-plugin.js +222 -34
- package/dist/src/plugins/maintenance-plugin.js.map +1 -1
- package/dist/src/plugins/postgres-plugin.d.ts +12 -0
- package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/src/plugins/postgres-plugin.js +319 -5
- package/dist/src/plugins/postgres-plugin.js.map +1 -1
- package/dist/ui/src/components/ControlPanelApp.d.ts.map +1 -1
- package/dist/ui/src/components/ControlPanelApp.js +4 -3
- package/dist/ui/src/components/ControlPanelApp.js.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.js +3 -1
- package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js +17 -4
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js +5 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +4 -2
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +12 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js +174 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +6 -3
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +256 -16
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/index.d.ts +1 -0
- package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/index.js +1 -0
- package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
- package/dist-ui/assets/index-BkGp7ZKd.js +529 -0
- package/dist-ui/assets/index-BkGp7ZKd.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/index.js +3735 -3187
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +11 -0
- package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
- package/dist-ui-lib/src/dashboard/widgets/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/core/control-panel.ts +5 -5
- package/src/core/gateway.ts +135 -15
- package/src/core/plugin-registry.ts +171 -0
- package/src/index.ts +2 -0
- package/src/plugins/api-keys/api-keys-plugin.ts +58 -1
- package/src/plugins/api-keys/index.ts +1 -0
- package/src/plugins/api-keys/stores/postgres-store.ts +90 -67
- package/src/plugins/api-keys/types.ts +14 -1
- package/src/plugins/diagnostics-plugin.ts +77 -0
- package/src/plugins/index.ts +1 -1
- package/src/plugins/maintenance/SeedExecutor.tsx +9 -1
- package/src/plugins/maintenance/SeedList.tsx +85 -38
- package/src/plugins/maintenance/SeedManagementPage.tsx +10 -4
- package/src/plugins/maintenance/seed-executor.ts +56 -17
- package/src/plugins/maintenance-plugin.ts +267 -36
- package/src/plugins/postgres-plugin.ts +410 -5
- package/ui/src/App.tsx +3 -3
- package/ui/src/components/ControlPanelApp.tsx +4 -3
- package/ui/src/dashboard/builtInWidgets.tsx +3 -0
- package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +17 -4
- package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +5 -1
- package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -2
- package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +410 -0
- package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -3
- package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +533 -49
- package/ui/src/dashboard/widgets/index.ts +1 -0
- package/dist-ui/assets/index-0gzisPdy.js +0 -528
- package/dist-ui/assets/index-0gzisPdy.js.map +0 -1
|
@@ -40,33 +40,33 @@ const MAX_OUTPUT_SIZE = 100 * 1024;
|
|
|
40
40
|
/**
|
|
41
41
|
* Validate script path to prevent path traversal attacks
|
|
42
42
|
*
|
|
43
|
-
* @param
|
|
43
|
+
* @param scriptPath - Relative path to the script (e.g., "database/001.init.mjs" or "01-Setup/seed-data.mjs")
|
|
44
44
|
* @param scriptsPath - Base path for scripts directory
|
|
45
|
-
* @returns Resolved path if valid, null if invalid
|
|
45
|
+
* @returns Resolved absolute path if valid, null if invalid
|
|
46
46
|
*/
|
|
47
|
-
export function validateScriptPath(
|
|
48
|
-
// Only allow
|
|
49
|
-
if (
|
|
47
|
+
export function validateScriptPath(scriptPath: string, scriptsPath: string): string | null {
|
|
48
|
+
// Only allow .mjs files
|
|
49
|
+
if (!scriptPath.endsWith('.mjs')) {
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// Resolve paths
|
|
54
54
|
const basePath = resolve(scriptsPath);
|
|
55
|
-
const
|
|
55
|
+
const resolvedScriptPath = resolve(basePath, scriptPath);
|
|
56
56
|
|
|
57
57
|
// Ensure resolved path is within scriptsPath (prevent path traversal)
|
|
58
58
|
// Use relative() for platform-agnostic check (works on Windows and Unix)
|
|
59
|
-
const relativePath = relative(basePath,
|
|
59
|
+
const relativePath = relative(basePath, resolvedScriptPath);
|
|
60
60
|
if (relativePath.startsWith('..') || relativePath.includes('../') || relativePath.includes('..\\')) {
|
|
61
61
|
return null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Ensure file exists
|
|
65
|
-
if (!existsSync(
|
|
65
|
+
if (!existsSync(resolvedScriptPath)) {
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
return
|
|
69
|
+
return resolvedScriptPath;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -95,30 +95,64 @@ export class SeedExecutor {
|
|
|
95
95
|
*
|
|
96
96
|
* @param scriptPath - Absolute path to the script
|
|
97
97
|
* @param res - Express response object (for SSE streaming)
|
|
98
|
+
* @param databaseUrl - Optional database URL to pass to the script
|
|
99
|
+
* @param projectRoot - Optional project root directory (defaults to script's directory)
|
|
98
100
|
* @returns Promise resolving to execution result
|
|
99
101
|
*/
|
|
100
|
-
async execute(scriptPath: string, res: Response): Promise<SeedExecutionResult> {
|
|
102
|
+
async execute(scriptPath: string, res: Response, databaseUrl?: string, projectRoot?: string): Promise<SeedExecutionResult> {
|
|
101
103
|
this.startTime = Date.now();
|
|
102
104
|
this.outputBuffer = '';
|
|
103
105
|
this.errorBuffer = '';
|
|
104
106
|
this.outputSize = 0;
|
|
105
107
|
|
|
106
108
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
107
|
-
//
|
|
108
|
-
// Use
|
|
109
|
-
|
|
109
|
+
// Determine if we need tsx (for .ts/.mts files that import TS modules)
|
|
110
|
+
// Use tsx for .mjs files too since they might import from TS source
|
|
111
|
+
const needsTsx = scriptPath.match(/\.(mjs|ts|mts)$/);
|
|
112
|
+
const execCommand = needsTsx ? 'tsx' : process.execPath;
|
|
113
|
+
const execArgs = needsTsx ? [scriptPath] : [scriptPath];
|
|
114
|
+
|
|
115
|
+
// Spawn process with TypeScript support if needed
|
|
116
|
+
this.child = spawn(execCommand, execArgs, {
|
|
110
117
|
env: {
|
|
118
|
+
...process.env, // Inherit all env vars for tsx to work properly
|
|
111
119
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
112
|
-
DATABASE_URI: process.env.DATABASE_URI,
|
|
113
|
-
DATABASE_URL: process.env.DATABASE_URL,
|
|
120
|
+
DATABASE_URI: databaseUrl || process.env.DATABASE_URI,
|
|
121
|
+
DATABASE_URL: databaseUrl || process.env.DATABASE_URL,
|
|
114
122
|
PAYLOAD_PUBLIC_SERVER_URL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
|
|
115
123
|
NEXT_PUBLIC_SERVER_URL: process.env.NEXT_PUBLIC_SERVER_URL,
|
|
116
124
|
API_URL: process.env.API_URL,
|
|
125
|
+
PAYLOAD_SECRET: process.env.PAYLOAD_SECRET || 'dev-secret-key-change-in-production',
|
|
117
126
|
},
|
|
118
|
-
stdio: ['
|
|
119
|
-
cwd: resolve(scriptPath, '..'), // Run from scripts directory
|
|
127
|
+
stdio: ['pipe', 'pipe', 'pipe'], // stdin: pipe, stdout: pipe, stderr: pipe
|
|
128
|
+
cwd: projectRoot || resolve(scriptPath, '..'), // Run from project root (or scripts directory if not provided)
|
|
120
129
|
});
|
|
121
130
|
|
|
131
|
+
// Auto-answer interactive prompts (like Drizzle's schema push confirmation)
|
|
132
|
+
// Send 'y\n' every 500ms to handle any prompts that appear
|
|
133
|
+
const stdinInterval = setInterval(() => {
|
|
134
|
+
if (this.child && this.child.stdin && !this.child.stdin.destroyed) {
|
|
135
|
+
try {
|
|
136
|
+
this.child.stdin.write('y\n');
|
|
137
|
+
} catch (err) {
|
|
138
|
+
// Ignore errors - stdin might be closed
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}, 500);
|
|
142
|
+
|
|
143
|
+
// Clear interval when process exits
|
|
144
|
+
const cleanupInterval = () => {
|
|
145
|
+
clearInterval(stdinInterval);
|
|
146
|
+
// Close stdin to signal EOF
|
|
147
|
+
if (this.child && this.child.stdin && !this.child.stdin.destroyed) {
|
|
148
|
+
try {
|
|
149
|
+
this.child.stdin.end();
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// Ignore errors
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
122
156
|
// Handle stdout
|
|
123
157
|
this.child.stdout?.on('data', (data: Buffer) => {
|
|
124
158
|
const text = data.toString();
|
|
@@ -165,6 +199,8 @@ export class SeedExecutor {
|
|
|
165
199
|
|
|
166
200
|
// Handle process exit
|
|
167
201
|
this.child.on('exit', (code, signal) => {
|
|
202
|
+
cleanupInterval(); // Clean up stdin interval
|
|
203
|
+
|
|
168
204
|
const exitCode = code ?? (signal ? 1 : 0);
|
|
169
205
|
const duration = Date.now() - this.startTime;
|
|
170
206
|
|
|
@@ -188,6 +224,8 @@ export class SeedExecutor {
|
|
|
188
224
|
|
|
189
225
|
// Handle spawn errors
|
|
190
226
|
this.child.on('error', (err: Error) => {
|
|
227
|
+
cleanupInterval(); // Clean up stdin interval
|
|
228
|
+
|
|
191
229
|
sendSSEEvent(res, {
|
|
192
230
|
type: 'error',
|
|
193
231
|
data: err.message,
|
|
@@ -199,6 +237,7 @@ export class SeedExecutor {
|
|
|
199
237
|
|
|
200
238
|
// Handle SSE connection close - terminate child process
|
|
201
239
|
res.on('close', () => {
|
|
240
|
+
cleanupInterval(); // Clean up stdin interval
|
|
202
241
|
if (this.child && !this.child.killed) {
|
|
203
242
|
this.child.kill('SIGTERM');
|
|
204
243
|
}
|
|
@@ -10,13 +10,103 @@
|
|
|
10
10
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { readdirSync, statSync } from 'fs';
|
|
14
|
-
import { resolve } from 'path';
|
|
13
|
+
import { readdirSync, statSync, readFileSync } from 'fs';
|
|
14
|
+
import { resolve, join, relative } from 'path';
|
|
15
15
|
import type { Request, Response } from 'express';
|
|
16
16
|
import type { Plugin, PluginConfig, PluginRegistry } from '../core/plugin-registry.js';
|
|
17
17
|
import { SeedExecutor, validateScriptPath } from './maintenance/seed-executor.js';
|
|
18
18
|
import { getPostgres, hasPostgres } from './postgres-plugin.js';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Extract description from JSDoc comment at top of file
|
|
22
|
+
*/
|
|
23
|
+
function extractDescription(filePath: string): string | undefined {
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
26
|
+
// Match JSDoc comment at start of file (after any whitespace)
|
|
27
|
+
const jsdocMatch = content.match(/^\s*\/\*\*\s*\n([\s\S]*?)\*\//);
|
|
28
|
+
|
|
29
|
+
if (jsdocMatch) {
|
|
30
|
+
// Extract lines and clean up asterisks and whitespace
|
|
31
|
+
const lines = jsdocMatch[1]
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map(line => line.replace(/^\s*\*\s?/, '').trim())
|
|
34
|
+
.filter(line => line.length > 0);
|
|
35
|
+
|
|
36
|
+
// Skip the first line if it's just the title (will be shown separately)
|
|
37
|
+
// Return subsequent lines as the description
|
|
38
|
+
return lines.slice(1).join(' ').trim() || undefined;
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// Failed to read file or parse description
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Recursively scan directory for .mjs files
|
|
49
|
+
*/
|
|
50
|
+
function scanSeedScripts(dir: string, basePath: string = dir): any[] {
|
|
51
|
+
const results: any[] = [];
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
55
|
+
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const fullPath = join(dir, entry.name);
|
|
58
|
+
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
// Recursively scan subdirectories
|
|
61
|
+
results.push(...scanSeedScripts(fullPath, basePath));
|
|
62
|
+
} else if (entry.isFile() && entry.name.endsWith('.mjs')) {
|
|
63
|
+
// Found a .mjs file
|
|
64
|
+
const stats = statSync(fullPath);
|
|
65
|
+
const relativePath = relative(basePath, fullPath);
|
|
66
|
+
const description = extractDescription(fullPath);
|
|
67
|
+
|
|
68
|
+
results.push({
|
|
69
|
+
type: 'file',
|
|
70
|
+
name: entry.name,
|
|
71
|
+
path: relativePath, // Relative path from scripts directory
|
|
72
|
+
fullPath: fullPath, // Absolute path
|
|
73
|
+
size: stats.size,
|
|
74
|
+
createdAt: stats.birthtime,
|
|
75
|
+
modifiedAt: stats.mtime,
|
|
76
|
+
description, // Add description from JSDoc
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// Directory not accessible, return empty array
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Custom seed task handler
|
|
89
|
+
*/
|
|
90
|
+
export interface SeedTaskHandler {
|
|
91
|
+
(options?: Record<string, any>, res?: Response): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Custom seed task definition
|
|
96
|
+
*/
|
|
97
|
+
export interface SeedTask {
|
|
98
|
+
/** Unique task identifier */
|
|
99
|
+
id: string;
|
|
100
|
+
/** Display name */
|
|
101
|
+
name: string;
|
|
102
|
+
/** Description of what this task does */
|
|
103
|
+
description: string;
|
|
104
|
+
/** Task handler function */
|
|
105
|
+
handler: SeedTaskHandler;
|
|
106
|
+
/** Optional task options/parameters */
|
|
107
|
+
options?: Record<string, any>;
|
|
108
|
+
}
|
|
109
|
+
|
|
20
110
|
export interface MaintenancePluginConfig {
|
|
21
111
|
/** Path to scripts directory (default: './scripts') */
|
|
22
112
|
scriptsPath?: string;
|
|
@@ -41,6 +131,9 @@ export interface MaintenancePluginConfig {
|
|
|
41
131
|
|
|
42
132
|
/** Enable database operations (default: true) */
|
|
43
133
|
enableDatabaseOps?: boolean;
|
|
134
|
+
|
|
135
|
+
/** Custom seed tasks */
|
|
136
|
+
customTasks?: SeedTask[];
|
|
44
137
|
}
|
|
45
138
|
|
|
46
139
|
/**
|
|
@@ -60,6 +153,38 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
|
|
|
60
153
|
const logger = registry.getLogger('maintenance');
|
|
61
154
|
logger.info('Maintenance plugin starting...');
|
|
62
155
|
|
|
156
|
+
// Initialize seed_executions table if PostgreSQL is available
|
|
157
|
+
if (hasPostgres()) {
|
|
158
|
+
try {
|
|
159
|
+
const db = getPostgres();
|
|
160
|
+
await db.queryRaw(`
|
|
161
|
+
CREATE TABLE IF NOT EXISTS seed_executions (
|
|
162
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
163
|
+
name TEXT NOT NULL,
|
|
164
|
+
status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'failed')),
|
|
165
|
+
started_at TIMESTAMPTZ NOT NULL,
|
|
166
|
+
completed_at TIMESTAMPTZ,
|
|
167
|
+
exit_code INTEGER,
|
|
168
|
+
output TEXT,
|
|
169
|
+
error TEXT,
|
|
170
|
+
duration_ms INTEGER,
|
|
171
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
172
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
173
|
+
)
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
// Create index on status for faster queries
|
|
177
|
+
await db.queryRaw(`
|
|
178
|
+
CREATE INDEX IF NOT EXISTS idx_seed_executions_status
|
|
179
|
+
ON seed_executions(status)
|
|
180
|
+
`);
|
|
181
|
+
|
|
182
|
+
logger.debug('Seed executions table initialized');
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.error('Failed to initialize seed_executions table', { error });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
63
188
|
// Clean up orphaned executions from previous crashes
|
|
64
189
|
if (hasPostgres()) {
|
|
65
190
|
try {
|
|
@@ -112,7 +237,7 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
|
|
|
112
237
|
logger.warn('Seed management requires PostgreSQL plugin for execution history');
|
|
113
238
|
}
|
|
114
239
|
|
|
115
|
-
// GET /seeds/discover - List available seed scripts
|
|
240
|
+
// GET /seeds/discover - List available seed scripts and custom tasks
|
|
116
241
|
registry.addRoute({
|
|
117
242
|
method: 'get',
|
|
118
243
|
path: '/seeds/discover',
|
|
@@ -120,25 +245,30 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
|
|
|
120
245
|
handler: (_req: Request, res: Response) => {
|
|
121
246
|
try {
|
|
122
247
|
const resolvedPath = resolve(scriptsPath);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
248
|
+
let seedFiles: any[] = [];
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
// Recursively scan for .mjs files
|
|
252
|
+
seedFiles = scanSeedScripts(resolvedPath);
|
|
253
|
+
|
|
254
|
+
// Sort by relative path (natural ordering respects numbered prefixes)
|
|
255
|
+
// Example: "01-Setup/001.init.mjs" comes before "02-Production/001.seed.mjs"
|
|
256
|
+
seedFiles.sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true }));
|
|
257
|
+
} catch (err) {
|
|
258
|
+
// Scripts directory may not exist, which is fine if we have custom tasks
|
|
259
|
+
logger.debug('Scripts directory not found or not readable');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Add custom tasks
|
|
263
|
+
const customTasks = (config.customTasks || []).map((task) => ({
|
|
264
|
+
type: 'task',
|
|
265
|
+
id: task.id,
|
|
266
|
+
name: task.name,
|
|
267
|
+
description: task.description,
|
|
268
|
+
options: task.options,
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
res.json({ seeds: [...seedFiles, ...customTasks] });
|
|
142
272
|
} catch (error) {
|
|
143
273
|
logger.error('Failed to discover seed scripts', { error });
|
|
144
274
|
res.status(500).json({
|
|
@@ -149,23 +279,45 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
|
|
|
149
279
|
},
|
|
150
280
|
});
|
|
151
281
|
|
|
152
|
-
// POST /seeds/execute - Execute a seed script
|
|
282
|
+
// POST /seeds/execute - Execute a seed script or custom task
|
|
153
283
|
registry.addRoute({
|
|
154
284
|
method: 'post',
|
|
155
285
|
path: '/seeds/execute',
|
|
156
286
|
pluginId: 'maintenance',
|
|
157
287
|
handler: async (req: Request, res: Response) => {
|
|
158
288
|
try {
|
|
159
|
-
const { name } = req.body;
|
|
289
|
+
const { name, type, options } = req.body;
|
|
160
290
|
|
|
161
291
|
if (!name || typeof name !== 'string') {
|
|
162
292
|
return res.status(400).json({ error: 'Script name is required' });
|
|
163
293
|
}
|
|
164
294
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
295
|
+
// Ensure seed_executions table exists (lazy initialization)
|
|
296
|
+
if (hasPostgres()) {
|
|
297
|
+
const db = getPostgres();
|
|
298
|
+
try {
|
|
299
|
+
await db.queryRaw(`
|
|
300
|
+
CREATE TABLE IF NOT EXISTS seed_executions (
|
|
301
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
302
|
+
name TEXT NOT NULL,
|
|
303
|
+
status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'failed')),
|
|
304
|
+
started_at TIMESTAMPTZ NOT NULL,
|
|
305
|
+
completed_at TIMESTAMPTZ,
|
|
306
|
+
exit_code INTEGER,
|
|
307
|
+
output TEXT,
|
|
308
|
+
error TEXT,
|
|
309
|
+
duration_ms INTEGER,
|
|
310
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
311
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
312
|
+
)
|
|
313
|
+
`);
|
|
314
|
+
await db.queryRaw(`
|
|
315
|
+
CREATE INDEX IF NOT EXISTS idx_seed_executions_status
|
|
316
|
+
ON seed_executions(status)
|
|
317
|
+
`);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
logger.debug('Table initialization check', { err });
|
|
320
|
+
}
|
|
169
321
|
}
|
|
170
322
|
|
|
171
323
|
// Check for concurrent execution
|
|
@@ -204,10 +356,39 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
|
|
|
204
356
|
executionId = result?.id || null;
|
|
205
357
|
}
|
|
206
358
|
|
|
207
|
-
|
|
208
|
-
|
|
359
|
+
const startTime = Date.now();
|
|
360
|
+
let exitCode = 0;
|
|
361
|
+
let output = '';
|
|
362
|
+
let error = '';
|
|
363
|
+
|
|
209
364
|
try {
|
|
210
|
-
|
|
365
|
+
// Execute based on type
|
|
366
|
+
if (type === 'task') {
|
|
367
|
+
// Find custom task
|
|
368
|
+
const task = (config.customTasks || []).find((t) => t.id === name);
|
|
369
|
+
if (!task) {
|
|
370
|
+
throw new Error(`Custom task not found: ${name}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Execute custom task handler with SSE streaming
|
|
374
|
+
await task.handler(options || {}, res);
|
|
375
|
+
} else {
|
|
376
|
+
// Execute file-based seed script
|
|
377
|
+
const scriptPath = validateScriptPath(name, scriptsPath);
|
|
378
|
+
if (!scriptPath) {
|
|
379
|
+
throw new Error('Invalid script name or file not found');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const executor = new SeedExecutor();
|
|
383
|
+
// Project root is one level up from scripts directory
|
|
384
|
+
const projectRoot = resolve(scriptsPath, '..');
|
|
385
|
+
const result = await executor.execute(scriptPath, res, config.databaseUrl, projectRoot);
|
|
386
|
+
exitCode = result.exitCode;
|
|
387
|
+
output = result.output;
|
|
388
|
+
error = result.error;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const duration = Date.now() - startTime;
|
|
211
392
|
|
|
212
393
|
// Update execution record
|
|
213
394
|
if (hasPostgres() && executionId) {
|
|
@@ -218,11 +399,11 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
|
|
|
218
399
|
output = $3, error = $4, duration_ms = $5, updated_at = NOW()
|
|
219
400
|
WHERE id = $6`,
|
|
220
401
|
[
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
402
|
+
exitCode === 0 ? 'completed' : 'failed',
|
|
403
|
+
exitCode,
|
|
404
|
+
output,
|
|
405
|
+
error,
|
|
406
|
+
duration,
|
|
226
407
|
executionId,
|
|
227
408
|
]
|
|
228
409
|
);
|
|
@@ -262,6 +443,56 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
|
|
|
262
443
|
},
|
|
263
444
|
});
|
|
264
445
|
|
|
446
|
+
// POST /database/reset - Drop and recreate database schema (local/dev only)
|
|
447
|
+
registry.addRoute({
|
|
448
|
+
method: 'post',
|
|
449
|
+
path: '/database/reset',
|
|
450
|
+
pluginId: 'maintenance',
|
|
451
|
+
handler: async (req: Request, res: Response) => {
|
|
452
|
+
try {
|
|
453
|
+
// Security: Only allow in local or development environments
|
|
454
|
+
const nodeEnv = process.env.NODE_ENV?.toLowerCase();
|
|
455
|
+
if (nodeEnv !== 'local' && nodeEnv !== 'development') {
|
|
456
|
+
return res.status(403).json({
|
|
457
|
+
error: 'Database reset is only available in local or development environments',
|
|
458
|
+
currentEnv: nodeEnv || 'production',
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!hasPostgres()) {
|
|
463
|
+
return res.status(503).json({
|
|
464
|
+
error: 'PostgreSQL plugin required for database reset',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const db = getPostgres();
|
|
469
|
+
|
|
470
|
+
// Drop and recreate public schema (removes all tables, data, etc.)
|
|
471
|
+
await db.queryRaw('DROP SCHEMA IF EXISTS public CASCADE');
|
|
472
|
+
await db.queryRaw('CREATE SCHEMA public');
|
|
473
|
+
await db.queryRaw('GRANT ALL ON SCHEMA public TO public');
|
|
474
|
+
await db.queryRaw('GRANT ALL ON SCHEMA public TO postgres');
|
|
475
|
+
await db.queryRaw('GRANT ALL ON SCHEMA public TO qwickapps');
|
|
476
|
+
|
|
477
|
+
// Grant default privileges for future tables and sequences
|
|
478
|
+
await db.queryRaw('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO qwickapps');
|
|
479
|
+
await db.queryRaw('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO qwickapps');
|
|
480
|
+
|
|
481
|
+
res.json({
|
|
482
|
+
success: true,
|
|
483
|
+
message: 'Database schema has been reset. All tables and data have been deleted.',
|
|
484
|
+
timestamp: new Date().toISOString(),
|
|
485
|
+
});
|
|
486
|
+
} catch (error) {
|
|
487
|
+
logger.error('Database reset failed', { error });
|
|
488
|
+
res.status(500).json({
|
|
489
|
+
error: 'Failed to reset database',
|
|
490
|
+
message: error instanceof Error ? error.message : String(error),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
265
496
|
// GET /seeds/history - List execution history
|
|
266
497
|
registry.addRoute({
|
|
267
498
|
method: 'get',
|