@package-broker/adapter-node 0.3.9 → 0.5.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 +48 -0
- package/src/drivers/sqlite-driver.ts +66 -13
- package/src/index.ts +99 -36
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Database Migration Script
|
|
5
|
+
* Runs Drizzle migrations for SQLite database
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node scripts/migrate.js <db_path> [migrations_path]
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* node scripts/migrate.js /data/database.sqlite
|
|
12
|
+
* node scripts/migrate.js /data/database.sqlite /app/packages/main/migrations
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { SqliteDriver } = require('../dist/drivers/sqlite-driver.cjs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const dbPath = process.argv[2];
|
|
19
|
+
const migrationsPath = process.argv[3] || 'packages/main/migrations';
|
|
20
|
+
|
|
21
|
+
if (!dbPath) {
|
|
22
|
+
console.error('❌ Error: Database path is required');
|
|
23
|
+
console.error('Usage: node scripts/migrate.js <db_path> [migrations_path]');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function runMigrations() {
|
|
28
|
+
try {
|
|
29
|
+
console.log(`Initializing database at: ${dbPath}`);
|
|
30
|
+
const driver = new SqliteDriver(dbPath);
|
|
31
|
+
|
|
32
|
+
const absoluteMigrationsPath = path.isAbsolute(migrationsPath)
|
|
33
|
+
? migrationsPath
|
|
34
|
+
: path.join(process.cwd(), migrationsPath);
|
|
35
|
+
|
|
36
|
+
console.log(`Running migrations from: ${absoluteMigrationsPath}`);
|
|
37
|
+
await driver.runMigrations(absoluteMigrationsPath);
|
|
38
|
+
|
|
39
|
+
console.log('✅ Database migrations completed successfully');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('❌ Migration failed:', error.message);
|
|
43
|
+
console.error(error.stack);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
runMigrations();
|
|
@@ -1,23 +1,76 @@
|
|
|
1
|
-
|
|
2
1
|
import Database from 'better-sqlite3';
|
|
3
2
|
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
4
3
|
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
|
5
|
-
import { schema, type DatabasePort } from '@package-broker/core';
|
|
4
|
+
import { schema, type DatabasePort, type DatabaseDriver } from '@package-broker/core';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
export interface SqliteConnection {
|
|
8
|
+
db: DatabasePort;
|
|
9
|
+
sqlite: Database.Database;
|
|
10
|
+
}
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Create a SQLite database connection
|
|
14
|
+
*/
|
|
15
|
+
export function createSqliteDatabase(dbPath: string): SqliteConnection {
|
|
8
16
|
const sqlite = new Database(dbPath);
|
|
9
17
|
const db = drizzle(sqlite, { schema });
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
// but usually generic ORM usages work fine.
|
|
13
|
-
// Note: DatabasePort in core is currently aliased to DrizzleD1Database<typeof schema>.
|
|
14
|
-
// We need to make sure core/db/index.ts types are flexible enough.
|
|
15
|
-
// Ideally, core should export a generic Drizzle type.
|
|
16
|
-
|
|
17
|
-
return db as any;
|
|
19
|
+
return { db: db as any, sqlite };
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* SQLite Database Driver
|
|
24
|
+
* Implements DatabaseDriver interface for SQLite using Drizzle ORM
|
|
25
|
+
*/
|
|
26
|
+
export class SqliteDriver implements DatabaseDriver {
|
|
27
|
+
private connection: SqliteConnection;
|
|
28
|
+
|
|
29
|
+
constructor(dbPath: string) {
|
|
30
|
+
this.connection = createSqliteDatabase(dbPath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if database is initialized by checking for Drizzle's migrations table
|
|
35
|
+
* This is database-agnostic as Drizzle uses __drizzle_migrations for all databases
|
|
36
|
+
*/
|
|
37
|
+
async isInitialized(): Promise<boolean> {
|
|
38
|
+
const { sqlite } = this.connection;
|
|
39
|
+
try {
|
|
40
|
+
// Check for Drizzle's migration tracking table (database-agnostic approach)
|
|
41
|
+
const result = sqlite.prepare(
|
|
42
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='__drizzle_migrations'"
|
|
43
|
+
).get();
|
|
44
|
+
return !!result;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run migrations using Drizzle's migrator
|
|
52
|
+
* Uses migration files from the specified path (e.g., packages/main/migrations)
|
|
53
|
+
*/
|
|
54
|
+
async runMigrations(migrationsPath: string): Promise<void> {
|
|
55
|
+
const { db } = this.connection;
|
|
56
|
+
const absolutePath = path.isAbsolute(migrationsPath)
|
|
57
|
+
? migrationsPath
|
|
58
|
+
: path.join(process.cwd(), migrationsPath);
|
|
59
|
+
|
|
60
|
+
await migrate(db as any, { migrationsFolder: absolutePath });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the database connection/ORM instance
|
|
65
|
+
*/
|
|
66
|
+
getConnection(): DatabasePort {
|
|
67
|
+
return this.connection.db;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the raw SQLite connection (for driver-specific operations if needed)
|
|
72
|
+
*/
|
|
73
|
+
getSqliteConnection(): Database.Database {
|
|
74
|
+
return this.connection.sqlite;
|
|
75
|
+
}
|
|
23
76
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
1
|
import { serve } from '@hono/node-server';
|
|
3
|
-
import {
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { createApp, type AppInstance, type DatabaseDriver } from '@package-broker/core';
|
|
4
4
|
import { config } from 'dotenv';
|
|
5
|
-
import {
|
|
5
|
+
import { SqliteDriver } from './drivers/sqlite-driver.js';
|
|
6
6
|
import { FileSystemDriver } from './drivers/fs-driver.js';
|
|
7
7
|
import { RedisDriver } from './drivers/redis-driver.js';
|
|
8
8
|
import { MemoryCacheDriver, MemoryQueueDriver } from '@package-broker/core';
|
|
@@ -23,21 +23,51 @@ const STORAGE_PATH = process.env.STORAGE_PATH || './storage';
|
|
|
23
23
|
const CACHE_DRIVER = process.env.CACHE_DRIVER || 'memory';
|
|
24
24
|
const CACHE_URL = process.env.CACHE_URL || 'redis://localhost:6379';
|
|
25
25
|
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';
|
|
26
29
|
|
|
27
30
|
console.log('Starting PACKAGE.broker Node Adapter...');
|
|
28
31
|
console.log(`Configuration: DB=${DB_DRIVER}, STORAGE=${STORAGE_DRIVER}, CACHE=${CACHE_DRIVER}, QUEUE=${QUEUE_DRIVER}`);
|
|
29
32
|
|
|
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
|
+
|
|
30
45
|
async function start() {
|
|
31
|
-
// Initialize
|
|
32
|
-
let
|
|
46
|
+
// Initialize Database Driver (Port-Adapter pattern)
|
|
47
|
+
let databaseDriver: DatabaseDriver;
|
|
48
|
+
|
|
33
49
|
if (DB_DRIVER === 'sqlite') {
|
|
34
50
|
console.log(`Initializing SQLite at ${DB_URL}`);
|
|
35
|
-
|
|
36
|
-
// Auto-migrate on start (simplified for MVP)
|
|
37
|
-
// In real deployment, migrations should be separate step
|
|
51
|
+
databaseDriver = new SqliteDriver(DB_URL);
|
|
38
52
|
} else {
|
|
39
53
|
throw new Error(`Unsupported DB_DRIVER: ${DB_DRIVER}`);
|
|
40
54
|
}
|
|
55
|
+
|
|
56
|
+
// Check if database needs initialization (database-agnostic check)
|
|
57
|
+
const isDatabaseReady = await databaseDriver.isInitialized();
|
|
58
|
+
|
|
59
|
+
if (!isDatabaseReady) {
|
|
60
|
+
console.warn('');
|
|
61
|
+
console.warn('⚠️ DATABASE NOT INITIALIZED');
|
|
62
|
+
console.warn(' The database tables have not been created yet.');
|
|
63
|
+
console.warn(' A warning page will be shown to users until migrations are run.');
|
|
64
|
+
console.warn(` 📖 Documentation: ${DOCS_URL}`);
|
|
65
|
+
console.warn('');
|
|
66
|
+
} else {
|
|
67
|
+
console.log('Database initialized and ready.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const database = databaseDriver.getConnection();
|
|
41
71
|
|
|
42
72
|
let storage;
|
|
43
73
|
if (STORAGE_DRIVER === 'fs') {
|
|
@@ -70,40 +100,73 @@ async function start() {
|
|
|
70
100
|
queue = new MemoryQueueDriver();
|
|
71
101
|
}
|
|
72
102
|
|
|
73
|
-
//
|
|
103
|
+
// If database is not ready, return error for API requests
|
|
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)
|
|
74
142
|
const app = createApp({
|
|
75
143
|
database,
|
|
76
144
|
storage,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
appInstance.use('*', async (c: Context, next: Next) => {
|
|
80
|
-
// We already passed database/storage to createApp, but we can set extra vars here
|
|
81
|
-
// Note: createApp factory handles database/storage injection if passed in options
|
|
82
|
-
await next();
|
|
83
|
-
});
|
|
145
|
+
cache,
|
|
146
|
+
});
|
|
84
147
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
148
|
+
// Serve config.js dynamically (AFTER API routes)
|
|
149
|
+
app.get('/config.js', (c: Context) => {
|
|
150
|
+
return c.text(`window.env = { API_URL: "${process.env.API_URL || '/'}" };`, 200, {
|
|
151
|
+
'Content-Type': 'application/javascript',
|
|
152
|
+
});
|
|
153
|
+
});
|
|
91
154
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
155
|
+
// Static file serving and SPA fallback (MUST be AFTER API routes)
|
|
156
|
+
if (process.env.PUBLIC_DIR) {
|
|
157
|
+
console.log(`Serving static files from ${process.env.PUBLIC_DIR}`);
|
|
158
|
+
app.use('/*', serveStatic({ root: process.env.PUBLIC_DIR }));
|
|
159
|
+
|
|
160
|
+
// SPA Fallback for client-side routing
|
|
161
|
+
// This catches any routes that weren't handled by API routes or static files
|
|
162
|
+
app.get('*', async (c: Context) => {
|
|
163
|
+
try {
|
|
164
|
+
return c.html(await readFile(path.join(process.env.PUBLIC_DIR!, 'index.html'), 'utf-8'));
|
|
165
|
+
} catch (e) {
|
|
166
|
+
return c.text('Not Found', 404);
|
|
104
167
|
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
107
170
|
|
|
108
171
|
console.log(`Server listening on port ${PORT}`);
|
|
109
172
|
serve({
|