@package-broker/adapter-node 0.3.9 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@package-broker/adapter-node",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "keywords": [],
6
6
  "scripts": {
@@ -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
- export function createSqliteDatabase(dbPath: string): DatabasePort {
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
- // Create method to satisfy the DatabasePort interface if strictly typed,
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
- export async function migrateSqliteDatabase(db: ReturnType<typeof drizzle>, migrationsFolder: string) {
21
- // This helper runs migrations on startup
22
- await migrate(db, { migrationsFolder });
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 { createApp, type AppInstance } from '@package-broker/core';
2
+ import { Hono } from 'hono';
3
+ import { createApp, type AppInstance, type DatabaseDriver } from '@package-broker/core';
4
4
  import { config } from 'dotenv';
5
- import { createSqliteDatabase, migrateSqliteDatabase } from './drivers/sqlite-driver.js';
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 Drivers
32
- let database;
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
- database = createSqliteDatabase(DB_URL);
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
- // Create App
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
- onInit: (appInstance: AppInstance) => {
78
- // Inject non-standard drivers if needed or custom middleware
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
- // Serve config.js dynamically
86
- appInstance.get('/config.js', (c: Context) => {
87
- return c.text(`window.env = { API_URL: "${process.env.API_URL || '/'}" };`, 200, {
88
- 'Content-Type': 'application/javascript',
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
- if (process.env.PUBLIC_DIR) {
93
- console.log(`Serving static files from ${process.env.PUBLIC_DIR}`);
94
- appInstance.use('/*', serveStatic({ root: process.env.PUBLIC_DIR }));
95
-
96
- // SPA Fallback
97
- appInstance.get('*', async (c: Context) => {
98
- try {
99
- return c.html(await readFile(path.join(process.env.PUBLIC_DIR!, 'index.html'), 'utf-8'));
100
- } catch (e) {
101
- return c.text('Not Found', 404);
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({