@package-broker/adapter-node 0.5.0 → 0.7.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/package.json +1 -1
- package/scripts/migrate.cjs +73 -11
- package/src/drivers/sqlite-driver.ts +3 -4
- package/src/index.ts +20 -74
package/package.json
CHANGED
package/scripts/migrate.cjs
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Database Migration Script
|
|
5
|
-
* Runs
|
|
4
|
+
* Database Migration Script (Self-contained)
|
|
5
|
+
* Runs SQL migrations for SQLite database
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* node scripts/migrate.
|
|
8
|
+
* node scripts/migrate.cjs <db_path> [migrations_path]
|
|
9
9
|
*
|
|
10
10
|
* Example:
|
|
11
|
-
* node scripts/migrate.
|
|
12
|
-
* node scripts/migrate.
|
|
11
|
+
* node scripts/migrate.cjs /data/database.sqlite
|
|
12
|
+
* node scripts/migrate.cjs /data/database.sqlite /app/packages/main/migrations
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const Database = require('better-sqlite3');
|
|
16
|
+
const fs = require('fs');
|
|
16
17
|
const path = require('path');
|
|
17
18
|
|
|
18
19
|
const dbPath = process.argv[2];
|
|
@@ -20,23 +21,84 @@ const migrationsPath = process.argv[3] || 'packages/main/migrations';
|
|
|
20
21
|
|
|
21
22
|
if (!dbPath) {
|
|
22
23
|
console.error('❌ Error: Database path is required');
|
|
23
|
-
console.error('Usage: node scripts/migrate.
|
|
24
|
+
console.error('Usage: node scripts/migrate.cjs <db_path> [migrations_path]');
|
|
24
25
|
process.exit(1);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
function runMigrations() {
|
|
28
29
|
try {
|
|
29
30
|
console.log(`Initializing database at: ${dbPath}`);
|
|
30
|
-
|
|
31
|
+
|
|
32
|
+
// Create SQLite connection
|
|
33
|
+
const sqlite = new Database(dbPath);
|
|
31
34
|
|
|
32
35
|
const absoluteMigrationsPath = path.isAbsolute(migrationsPath)
|
|
33
36
|
? migrationsPath
|
|
34
37
|
: path.join(process.cwd(), migrationsPath);
|
|
35
38
|
|
|
36
39
|
console.log(`Running migrations from: ${absoluteMigrationsPath}`);
|
|
37
|
-
await driver.runMigrations(absoluteMigrationsPath);
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
// Create migrations tracking table if it doesn't exist
|
|
42
|
+
sqlite.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS __applied_migrations (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
name TEXT UNIQUE NOT NULL,
|
|
46
|
+
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
47
|
+
)
|
|
48
|
+
`);
|
|
49
|
+
|
|
50
|
+
// Get list of already applied migrations
|
|
51
|
+
const appliedMigrations = new Set(
|
|
52
|
+
sqlite.prepare('SELECT name FROM __applied_migrations').all().map(row => row.name)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Get all SQL migration files sorted by name
|
|
56
|
+
const migrationFiles = fs.readdirSync(absoluteMigrationsPath)
|
|
57
|
+
.filter(file => file.endsWith('.sql'))
|
|
58
|
+
.sort();
|
|
59
|
+
|
|
60
|
+
if (migrationFiles.length === 0) {
|
|
61
|
+
console.log('No migration files found.');
|
|
62
|
+
sqlite.close();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let appliedCount = 0;
|
|
67
|
+
|
|
68
|
+
for (const file of migrationFiles) {
|
|
69
|
+
if (appliedMigrations.has(file)) {
|
|
70
|
+
console.log(` ⏭️ ${file} (already applied)`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const filePath = path.join(absoluteMigrationsPath, file);
|
|
75
|
+
const sql = fs.readFileSync(filePath, 'utf-8');
|
|
76
|
+
|
|
77
|
+
console.log(` 📄 Applying: ${file}`);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Run migration in a transaction
|
|
81
|
+
sqlite.exec('BEGIN TRANSACTION');
|
|
82
|
+
sqlite.exec(sql);
|
|
83
|
+
sqlite.prepare('INSERT INTO __applied_migrations (name) VALUES (?)').run(file);
|
|
84
|
+
sqlite.exec('COMMIT');
|
|
85
|
+
appliedCount++;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
sqlite.exec('ROLLBACK');
|
|
88
|
+
// Check if error is due to already existing table/column (safe to ignore)
|
|
89
|
+
if (err.message.includes('already exists') || err.message.includes('duplicate column')) {
|
|
90
|
+
console.log(` ⚠️ ${file}: ${err.message} (continuing)`);
|
|
91
|
+
sqlite.prepare('INSERT OR IGNORE INTO __applied_migrations (name) VALUES (?)').run(file);
|
|
92
|
+
} else {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Close connection
|
|
99
|
+
sqlite.close();
|
|
100
|
+
|
|
101
|
+
console.log(`\n✅ Database migrations completed (${appliedCount} applied)`);
|
|
40
102
|
process.exit(0);
|
|
41
103
|
} catch (error) {
|
|
42
104
|
console.error('❌ Migration failed:', error.message);
|
|
@@ -31,15 +31,14 @@ export class SqliteDriver implements DatabaseDriver {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Check if database is initialized by checking for
|
|
35
|
-
* This is database-agnostic as Drizzle uses __drizzle_migrations for all databases
|
|
34
|
+
* Check if database is initialized by checking for the migrations tracking table
|
|
36
35
|
*/
|
|
37
36
|
async isInitialized(): Promise<boolean> {
|
|
38
37
|
const { sqlite } = this.connection;
|
|
39
38
|
try {
|
|
40
|
-
// Check for
|
|
39
|
+
// Check for migration tracking table created by migrate.cjs script
|
|
41
40
|
const result = sqlite.prepare(
|
|
42
|
-
"SELECT name FROM sqlite_master WHERE type='table' AND name='
|
|
41
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='__applied_migrations'"
|
|
43
42
|
).get();
|
|
44
43
|
return !!result;
|
|
45
44
|
} catch {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { serve } from '@hono/node-server';
|
|
2
|
-
import {
|
|
3
|
-
import { createApp, type AppInstance, type DatabaseDriver } from '@package-broker/core';
|
|
2
|
+
import { createApp } from '@package-broker/core';
|
|
4
3
|
import { config } from 'dotenv';
|
|
5
4
|
import { SqliteDriver } from './drivers/sqlite-driver.js';
|
|
6
5
|
import { FileSystemDriver } from './drivers/fs-driver.js';
|
|
@@ -9,7 +8,7 @@ import { MemoryCacheDriver, MemoryQueueDriver } from '@package-broker/core';
|
|
|
9
8
|
import path from 'node:path';
|
|
10
9
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
11
10
|
import { readFile } from 'node:fs/promises';
|
|
12
|
-
import type { Context
|
|
11
|
+
import type { Context } from 'hono';
|
|
13
12
|
|
|
14
13
|
// Load environment variables
|
|
15
14
|
config();
|
|
@@ -23,28 +22,13 @@ const STORAGE_PATH = process.env.STORAGE_PATH || './storage';
|
|
|
23
22
|
const CACHE_DRIVER = process.env.CACHE_DRIVER || 'memory';
|
|
24
23
|
const CACHE_URL = process.env.CACHE_URL || 'redis://localhost:6379';
|
|
25
24
|
const QUEUE_DRIVER = process.env.QUEUE_DRIVER || 'memory';
|
|
26
|
-
const MIGRATIONS_PATH = process.env.MIGRATIONS_PATH || 'packages/main/migrations';
|
|
27
|
-
|
|
28
|
-
const DOCS_URL = 'https://package-broker.github.io/docs/installation/docker';
|
|
29
25
|
|
|
30
26
|
console.log('Starting PACKAGE.broker Node Adapter...');
|
|
31
27
|
console.log(`Configuration: DB=${DB_DRIVER}, STORAGE=${STORAGE_DRIVER}, CACHE=${CACHE_DRIVER}, QUEUE=${QUEUE_DRIVER}`);
|
|
32
28
|
|
|
33
|
-
/**
|
|
34
|
-
* Get the database not initialized error response
|
|
35
|
-
* DRY: Single source of truth for error response structure
|
|
36
|
-
*/
|
|
37
|
-
function getDatabaseNotInitializedError() {
|
|
38
|
-
return {
|
|
39
|
-
error: 'DATABASE_NOT_INITIALIZED',
|
|
40
|
-
message: 'Database not initialized. Please run migrations.',
|
|
41
|
-
docsUrl: DOCS_URL
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
29
|
async function start() {
|
|
46
|
-
// Initialize Database Driver
|
|
47
|
-
let databaseDriver
|
|
30
|
+
// Initialize Database Driver
|
|
31
|
+
let databaseDriver;
|
|
48
32
|
|
|
49
33
|
if (DB_DRIVER === 'sqlite') {
|
|
50
34
|
console.log(`Initializing SQLite at ${DB_URL}`);
|
|
@@ -53,31 +37,31 @@ async function start() {
|
|
|
53
37
|
throw new Error(`Unsupported DB_DRIVER: ${DB_DRIVER}`);
|
|
54
38
|
}
|
|
55
39
|
|
|
56
|
-
// Check if database
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
if (!isDatabaseReady) {
|
|
40
|
+
// Check if database is initialized (logging only)
|
|
41
|
+
const isInitialized = await databaseDriver.isInitialized();
|
|
42
|
+
if (!isInitialized) {
|
|
60
43
|
console.warn('');
|
|
61
44
|
console.warn('⚠️ DATABASE NOT INITIALIZED');
|
|
62
|
-
console.warn('
|
|
63
|
-
console.warn('
|
|
64
|
-
console.warn(
|
|
45
|
+
console.warn(' Run migration scripts to initialize the database:');
|
|
46
|
+
console.warn('');
|
|
47
|
+
console.warn(' docker exec <container> node packages/adapter-node/scripts/migrate.cjs /data/database.sqlite');
|
|
48
|
+
console.warn('');
|
|
49
|
+
console.warn(' 📖 See: https://package.broker/docs/getting-started/quickstart-docker');
|
|
65
50
|
console.warn('');
|
|
66
|
-
} else {
|
|
67
|
-
console.log('Database initialized and ready.');
|
|
68
51
|
}
|
|
69
52
|
|
|
70
53
|
const database = databaseDriver.getConnection();
|
|
71
54
|
|
|
55
|
+
// Initialize Storage
|
|
72
56
|
let storage;
|
|
73
57
|
if (STORAGE_DRIVER === 'fs') {
|
|
74
58
|
console.log(`Initializing FS Storage at ${STORAGE_PATH}`);
|
|
75
59
|
storage = new FileSystemDriver(STORAGE_PATH);
|
|
76
60
|
} else {
|
|
77
|
-
// TODO: Add S3 support
|
|
78
61
|
throw new Error(`Unsupported STORAGE_DRIVER: ${STORAGE_DRIVER}`);
|
|
79
62
|
}
|
|
80
63
|
|
|
64
|
+
// Initialize Cache
|
|
81
65
|
let cache;
|
|
82
66
|
if (CACHE_DRIVER === 'redis') {
|
|
83
67
|
console.log(`Initializing Redis Cache at ${CACHE_URL}`);
|
|
@@ -87,10 +71,11 @@ async function start() {
|
|
|
87
71
|
cache = new MemoryCacheDriver();
|
|
88
72
|
}
|
|
89
73
|
|
|
74
|
+
// Initialize Queue
|
|
90
75
|
let queue;
|
|
91
76
|
if (QUEUE_DRIVER === 'redis') {
|
|
92
77
|
if (CACHE_DRIVER === 'redis') {
|
|
93
|
-
queue = cache as any;
|
|
78
|
+
queue = cache as any;
|
|
94
79
|
} else {
|
|
95
80
|
console.log(`Initializing Redis Queue at ${CACHE_URL}`);
|
|
96
81
|
queue = new RedisDriver(CACHE_URL);
|
|
@@ -100,65 +85,26 @@ async function start() {
|
|
|
100
85
|
queue = new MemoryQueueDriver();
|
|
101
86
|
}
|
|
102
87
|
|
|
103
|
-
//
|
|
104
|
-
if (!isDatabaseReady) {
|
|
105
|
-
const warningApp = new Hono();
|
|
106
|
-
const errorResponse = getDatabaseNotInitializedError();
|
|
107
|
-
|
|
108
|
-
// Health endpoint returns warning status (200 OK with error status in body for CI compatibility)
|
|
109
|
-
warningApp.get('/health', (c) => {
|
|
110
|
-
return c.json({
|
|
111
|
-
status: 'error',
|
|
112
|
-
...errorResponse
|
|
113
|
-
}, 200); // Return 200 for CI health checks, but include error status
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// All API requests return JSON error (BEFORE static files)
|
|
117
|
-
warningApp.all('/api/*', (c) => {
|
|
118
|
-
return c.json(errorResponse, 503);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Serve static files if PUBLIC_DIR is set (UI will display the error)
|
|
122
|
-
if (process.env.PUBLIC_DIR) {
|
|
123
|
-
warningApp.use('/*', serveStatic({ root: process.env.PUBLIC_DIR }));
|
|
124
|
-
warningApp.get('*', async (c) => {
|
|
125
|
-
try {
|
|
126
|
-
return c.html(await readFile(path.join(process.env.PUBLIC_DIR!, 'index.html'), 'utf-8'));
|
|
127
|
-
} catch (e) {
|
|
128
|
-
return c.text('Not Found', 404);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
console.log(`Server listening on port ${PORT} (WARNING: Database not initialized)`);
|
|
134
|
-
serve({
|
|
135
|
-
fetch: warningApp.fetch,
|
|
136
|
-
port: PORT
|
|
137
|
-
});
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Create App (API routes are registered inside createApp)
|
|
88
|
+
// Create App
|
|
142
89
|
const app = createApp({
|
|
143
90
|
database,
|
|
144
91
|
storage,
|
|
145
92
|
cache,
|
|
146
93
|
});
|
|
147
94
|
|
|
148
|
-
// Serve config.js dynamically
|
|
95
|
+
// Serve config.js dynamically
|
|
149
96
|
app.get('/config.js', (c: Context) => {
|
|
150
97
|
return c.text(`window.env = { API_URL: "${process.env.API_URL || '/'}" };`, 200, {
|
|
151
98
|
'Content-Type': 'application/javascript',
|
|
152
99
|
});
|
|
153
100
|
});
|
|
154
101
|
|
|
155
|
-
// Static file serving and SPA fallback
|
|
102
|
+
// Static file serving and SPA fallback
|
|
156
103
|
if (process.env.PUBLIC_DIR) {
|
|
157
104
|
console.log(`Serving static files from ${process.env.PUBLIC_DIR}`);
|
|
158
105
|
app.use('/*', serveStatic({ root: process.env.PUBLIC_DIR }));
|
|
159
106
|
|
|
160
|
-
// SPA Fallback
|
|
161
|
-
// This catches any routes that weren't handled by API routes or static files
|
|
107
|
+
// SPA Fallback
|
|
162
108
|
app.get('*', async (c: Context) => {
|
|
163
109
|
try {
|
|
164
110
|
return c.html(await readFile(path.join(process.env.PUBLIC_DIR!, 'index.html'), 'utf-8'));
|