s3db.js 11.3.2 → 12.0.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.
Files changed (83) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36945 -15510
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +66 -1
  5. package/dist/s3db.es.js +36914 -15534
  6. package/dist/s3db.es.js.map +1 -1
  7. package/mcp/entrypoint.js +58 -0
  8. package/mcp/tools/documentation.js +434 -0
  9. package/mcp/tools/index.js +4 -0
  10. package/package.json +35 -15
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +79 -49
  13. package/src/concerns/base62.js +85 -0
  14. package/src/concerns/dictionary-encoding.js +294 -0
  15. package/src/concerns/geo-encoding.js +256 -0
  16. package/src/concerns/high-performance-inserter.js +34 -30
  17. package/src/concerns/ip.js +325 -0
  18. package/src/concerns/metadata-encoding.js +345 -66
  19. package/src/concerns/money.js +193 -0
  20. package/src/concerns/partition-queue.js +7 -4
  21. package/src/concerns/plugin-storage.js +97 -47
  22. package/src/database.class.js +76 -74
  23. package/src/errors.js +0 -4
  24. package/src/plugins/api/auth/api-key-auth.js +88 -0
  25. package/src/plugins/api/auth/basic-auth.js +154 -0
  26. package/src/plugins/api/auth/index.js +112 -0
  27. package/src/plugins/api/auth/jwt-auth.js +169 -0
  28. package/src/plugins/api/index.js +544 -0
  29. package/src/plugins/api/middlewares/index.js +15 -0
  30. package/src/plugins/api/middlewares/validator.js +185 -0
  31. package/src/plugins/api/routes/auth-routes.js +241 -0
  32. package/src/plugins/api/routes/resource-routes.js +304 -0
  33. package/src/plugins/api/server.js +354 -0
  34. package/src/plugins/api/utils/error-handler.js +147 -0
  35. package/src/plugins/api/utils/openapi-generator.js +1240 -0
  36. package/src/plugins/api/utils/response-formatter.js +218 -0
  37. package/src/plugins/backup/streaming-exporter.js +132 -0
  38. package/src/plugins/backup.plugin.js +103 -50
  39. package/src/plugins/cache/s3-cache.class.js +95 -47
  40. package/src/plugins/cache.plugin.js +107 -9
  41. package/src/plugins/concerns/plugin-dependencies.js +313 -0
  42. package/src/plugins/concerns/prometheus-formatter.js +255 -0
  43. package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
  44. package/src/plugins/consumers/sqs-consumer.js +4 -0
  45. package/src/plugins/costs.plugin.js +255 -39
  46. package/src/plugins/eventual-consistency/helpers.js +15 -1
  47. package/src/plugins/geo.plugin.js +873 -0
  48. package/src/plugins/importer/index.js +1020 -0
  49. package/src/plugins/index.js +11 -0
  50. package/src/plugins/metrics.plugin.js +163 -4
  51. package/src/plugins/queue-consumer.plugin.js +6 -27
  52. package/src/plugins/relation.errors.js +139 -0
  53. package/src/plugins/relation.plugin.js +1242 -0
  54. package/src/plugins/replicator.plugin.js +2 -1
  55. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  56. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  57. package/src/plugins/replicators/index.js +28 -3
  58. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  59. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  60. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  61. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  62. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  63. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  64. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  65. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  66. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  67. package/src/plugins/state-machine.plugin.js +122 -68
  68. package/src/plugins/tfstate/README.md +745 -0
  69. package/src/plugins/tfstate/base-driver.js +80 -0
  70. package/src/plugins/tfstate/errors.js +112 -0
  71. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  72. package/src/plugins/tfstate/index.js +2660 -0
  73. package/src/plugins/tfstate/s3-driver.js +192 -0
  74. package/src/plugins/ttl.plugin.js +536 -0
  75. package/src/resource.class.js +315 -36
  76. package/src/s3db.d.ts +66 -1
  77. package/src/schema.class.js +366 -32
  78. package/SECURITY.md +0 -76
  79. package/src/partition-drivers/base-partition-driver.js +0 -106
  80. package/src/partition-drivers/index.js +0 -66
  81. package/src/partition-drivers/memory-partition-driver.js +0 -289
  82. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  83. package/src/partition-drivers/sync-partition-driver.js +0 -38
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Resource Routes - Dynamic RESTful routes for s3db.js resources
3
+ *
4
+ * Automatically generates REST endpoints for each resource
5
+ */
6
+
7
+ import { Hono } from 'hono';
8
+ import { asyncHandler } from '../utils/error-handler.js';
9
+ import * as formatter from '../utils/response-formatter.js';
10
+
11
+ /**
12
+ * Create routes for a resource
13
+ * @param {Object} resource - s3db.js Resource instance
14
+ * @param {string} version - Resource version (e.g., 'v1', 'v1')
15
+ * @param {Object} config - Route configuration
16
+ * @returns {Hono} Hono app with resource routes
17
+ */
18
+ export function createResourceRoutes(resource, version, config = {}) {
19
+ const app = new Hono();
20
+ const {
21
+ methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
22
+ customMiddleware = [],
23
+ enableValidation = true
24
+ } = config;
25
+
26
+ const resourceName = resource.name;
27
+ const basePath = `/${version}/${resourceName}`;
28
+
29
+ // Apply custom middleware
30
+ customMiddleware.forEach(middleware => {
31
+ app.use('*', middleware);
32
+ });
33
+
34
+ // LIST - GET /{version}/{resource}
35
+ if (methods.includes('GET')) {
36
+ app.get('/', asyncHandler(async (c) => {
37
+ const query = c.req.query();
38
+ const limit = parseInt(query.limit) || 100;
39
+ const offset = parseInt(query.offset) || 0;
40
+ const partition = query.partition;
41
+ const partitionValues = query.partitionValues
42
+ ? JSON.parse(query.partitionValues)
43
+ : undefined;
44
+
45
+ // Extract filters from query string (any key that's not limit, offset, partition, partitionValues, sort)
46
+ const reservedKeys = ['limit', 'offset', 'partition', 'partitionValues', 'sort'];
47
+ const filters = {};
48
+ for (const [key, value] of Object.entries(query)) {
49
+ if (!reservedKeys.includes(key)) {
50
+ // Try to parse as JSON for complex values
51
+ try {
52
+ filters[key] = JSON.parse(value);
53
+ } catch {
54
+ // Keep as string if not valid JSON
55
+ filters[key] = value;
56
+ }
57
+ }
58
+ }
59
+
60
+ let items;
61
+ let total;
62
+
63
+ // Use query if filters are present
64
+ if (Object.keys(filters).length > 0) {
65
+ items = await resource.query(filters, { limit: limit + offset });
66
+ items = items.slice(offset, offset + limit);
67
+ total = items.length;
68
+ } else if (partition && partitionValues) {
69
+ // Query specific partition
70
+ items = await resource.listPartition({
71
+ partition,
72
+ partitionValues,
73
+ limit: limit + offset
74
+ });
75
+ items = items.slice(offset, offset + limit);
76
+ total = items.length;
77
+ } else {
78
+ // Regular list
79
+ items = await resource.list({ limit: limit + offset });
80
+ items = items.slice(offset, offset + limit);
81
+ total = items.length;
82
+ }
83
+
84
+ const response = formatter.list(items, {
85
+ total,
86
+ page: Math.floor(offset / limit) + 1,
87
+ pageSize: limit,
88
+ pageCount: Math.ceil(total / limit)
89
+ });
90
+
91
+ // Set pagination headers
92
+ c.header('X-Total-Count', total.toString());
93
+ c.header('X-Page-Count', Math.ceil(total / limit).toString());
94
+
95
+ return c.json(response, response._status);
96
+ }));
97
+ }
98
+
99
+ // GET ONE - GET /{version}/{resource}/:id
100
+ if (methods.includes('GET')) {
101
+ app.get('/:id', asyncHandler(async (c) => {
102
+ const id = c.req.param('id');
103
+ const query = c.req.query();
104
+ const partition = query.partition;
105
+ const partitionValues = query.partitionValues
106
+ ? JSON.parse(query.partitionValues)
107
+ : undefined;
108
+
109
+ let item;
110
+
111
+ if (partition && partitionValues) {
112
+ // Get from specific partition
113
+ item = await resource.getFromPartition({
114
+ id,
115
+ partitionName: partition,
116
+ partitionValues
117
+ });
118
+ } else {
119
+ // Regular get
120
+ item = await resource.get(id);
121
+ }
122
+
123
+ if (!item) {
124
+ const response = formatter.notFound(resourceName, id);
125
+ return c.json(response, response._status);
126
+ }
127
+
128
+ const response = formatter.success(item);
129
+ return c.json(response, response._status);
130
+ }));
131
+ }
132
+
133
+ // CREATE - POST /{version}/{resource}
134
+ if (methods.includes('POST')) {
135
+ app.post('/', asyncHandler(async (c) => {
136
+ const data = await c.req.json();
137
+
138
+ // Validation middleware will run if enabled
139
+ const item = await resource.insert(data);
140
+
141
+ const location = `${basePath}/${item.id}`;
142
+ const response = formatter.created(item, location);
143
+
144
+ c.header('Location', location);
145
+ return c.json(response, response._status);
146
+ }));
147
+ }
148
+
149
+ // UPDATE (full) - PUT /{version}/{resource}/:id
150
+ if (methods.includes('PUT')) {
151
+ app.put('/:id', asyncHandler(async (c) => {
152
+ const id = c.req.param('id');
153
+ const data = await c.req.json();
154
+
155
+ // Check if exists
156
+ const existing = await resource.get(id);
157
+ if (!existing) {
158
+ const response = formatter.notFound(resourceName, id);
159
+ return c.json(response, response._status);
160
+ }
161
+
162
+ // Full update
163
+ const updated = await resource.update(id, data);
164
+
165
+ const response = formatter.success(updated);
166
+ return c.json(response, response._status);
167
+ }));
168
+ }
169
+
170
+ // UPDATE (partial) - PATCH /{version}/{resource}/:id
171
+ if (methods.includes('PATCH')) {
172
+ app.patch('/:id', asyncHandler(async (c) => {
173
+ const id = c.req.param('id');
174
+ const data = await c.req.json();
175
+
176
+ // Check if exists
177
+ const existing = await resource.get(id);
178
+ if (!existing) {
179
+ const response = formatter.notFound(resourceName, id);
180
+ return c.json(response, response._status);
181
+ }
182
+
183
+ // Partial update (merge with existing)
184
+ const merged = { ...existing, ...data, id };
185
+ const updated = await resource.update(id, merged);
186
+
187
+ const response = formatter.success(updated);
188
+ return c.json(response, response._status);
189
+ }));
190
+ }
191
+
192
+ // DELETE - DELETE /{version}/{resource}/:id
193
+ if (methods.includes('DELETE')) {
194
+ app.delete('/:id', asyncHandler(async (c) => {
195
+ const id = c.req.param('id');
196
+
197
+ // Check if exists
198
+ const existing = await resource.get(id);
199
+ if (!existing) {
200
+ const response = formatter.notFound(resourceName, id);
201
+ return c.json(response, response._status);
202
+ }
203
+
204
+ await resource.delete(id);
205
+
206
+ const response = formatter.noContent();
207
+ return c.json(response, response._status);
208
+ }));
209
+ }
210
+
211
+ // HEAD - HEAD /{version}/{resource}
212
+ if (methods.includes('HEAD')) {
213
+ app.on('HEAD', '/', asyncHandler(async (c) => {
214
+ // Get statistics
215
+ const total = await resource.count();
216
+
217
+ // Get all items to calculate stats (for small datasets)
218
+ // For large datasets, this might need optimization
219
+ const allItems = await resource.list({ limit: 1000 });
220
+
221
+ // Calculate statistics
222
+ const stats = {
223
+ total,
224
+ version: resource.config?.currentVersion || resource.version || 'v1'
225
+ };
226
+
227
+ // Add resource-specific stats
228
+ c.header('X-Total-Count', total.toString());
229
+ c.header('X-Resource-Version', stats.version);
230
+
231
+ // Add schema info
232
+ c.header('X-Schema-Fields', Object.keys(resource.config?.attributes || {}).length.toString());
233
+
234
+ return c.body(null, 200);
235
+ }));
236
+
237
+ app.on('HEAD', '/:id', asyncHandler(async (c) => {
238
+ const id = c.req.param('id');
239
+ const item = await resource.get(id);
240
+
241
+ if (!item) {
242
+ return c.body(null, 404);
243
+ }
244
+
245
+ // Add metadata headers
246
+ if (item.updatedAt) {
247
+ c.header('Last-Modified', new Date(item.updatedAt).toUTCString());
248
+ }
249
+
250
+ return c.body(null, 200);
251
+ }));
252
+ }
253
+
254
+ // OPTIONS - OPTIONS /{version}/{resource}
255
+ if (methods.includes('OPTIONS')) {
256
+ app.options('/', asyncHandler(async (c) => {
257
+ c.header('Allow', methods.join(', '));
258
+
259
+ // Return metadata about the resource
260
+ const total = await resource.count();
261
+ const schema = resource.config?.attributes || {};
262
+ const version = resource.config?.currentVersion || resource.version || 'v1';
263
+
264
+ const metadata = {
265
+ resource: resourceName,
266
+ version,
267
+ totalRecords: total,
268
+ allowedMethods: methods,
269
+ schema: Object.entries(schema).map(([name, def]) => ({
270
+ name,
271
+ type: typeof def === 'string' ? def.split('|')[0] : def.type,
272
+ rules: typeof def === 'string' ? def.split('|').slice(1) : []
273
+ })),
274
+ endpoints: {
275
+ list: `/${version}/${resourceName}`,
276
+ get: `/${version}/${resourceName}/:id`,
277
+ create: `/${version}/${resourceName}`,
278
+ update: `/${version}/${resourceName}/:id`,
279
+ delete: `/${version}/${resourceName}/:id`
280
+ },
281
+ queryParameters: {
282
+ limit: 'number (1-1000, default: 100)',
283
+ offset: 'number (min: 0, default: 0)',
284
+ partition: 'string (partition name)',
285
+ partitionValues: 'JSON string',
286
+ '[any field]': 'any (filter by field value)'
287
+ }
288
+ };
289
+
290
+ return c.json(metadata);
291
+ }));
292
+
293
+ app.options('/:id', (c) => {
294
+ c.header('Allow', methods.filter(m => m !== 'POST').join(', '));
295
+ return c.body(null, 204);
296
+ });
297
+ }
298
+
299
+ return app;
300
+ }
301
+
302
+ export default {
303
+ createResourceRoutes
304
+ };
@@ -0,0 +1,354 @@
1
+ /**
2
+ * API Server - Hono-based HTTP server for s3db.js API Plugin
3
+ *
4
+ * Manages HTTP server lifecycle and routing
5
+ */
6
+
7
+ import { Hono } from 'hono';
8
+ import { serve } from '@hono/node-server';
9
+ import { swaggerUI } from '@hono/swagger-ui';
10
+ import { createResourceRoutes } from './routes/resource-routes.js';
11
+ import { errorHandler } from './utils/error-handler.js';
12
+ import * as formatter from './utils/response-formatter.js';
13
+ import { generateOpenAPISpec } from './utils/openapi-generator.js';
14
+
15
+ /**
16
+ * API Server class
17
+ * @class
18
+ */
19
+ export class ApiServer {
20
+ /**
21
+ * Create API server
22
+ * @param {Object} options - Server options
23
+ * @param {number} options.port - Server port
24
+ * @param {string} options.host - Server host
25
+ * @param {Object} options.database - s3db.js database instance
26
+ * @param {Object} options.resources - Resource configuration
27
+ * @param {Array} options.middlewares - Global middlewares
28
+ */
29
+ constructor(options = {}) {
30
+ this.options = {
31
+ port: options.port || 3000,
32
+ host: options.host || '0.0.0.0',
33
+ database: options.database,
34
+ resources: options.resources || {},
35
+ middlewares: options.middlewares || [],
36
+ verbose: options.verbose || false,
37
+ auth: options.auth || {},
38
+ docsEnabled: options.docsEnabled !== false, // Enable /docs by default
39
+ docsUI: options.docsUI || 'redoc', // 'swagger' or 'redoc'
40
+ maxBodySize: options.maxBodySize || 10 * 1024 * 1024, // 10MB default
41
+ rootHandler: options.rootHandler, // Custom handler for root path, if not provided redirects to /docs
42
+ apiInfo: {
43
+ title: options.apiTitle || 's3db.js API',
44
+ version: options.apiVersion || '1.0.0',
45
+ description: options.apiDescription || 'Auto-generated REST API for s3db.js resources'
46
+ }
47
+ };
48
+
49
+ this.app = new Hono();
50
+ this.server = null;
51
+ this.isRunning = false;
52
+ this.openAPISpec = null;
53
+
54
+ this._setupRoutes();
55
+ }
56
+
57
+ /**
58
+ * Setup all routes
59
+ * @private
60
+ */
61
+ _setupRoutes() {
62
+ // Apply global middlewares
63
+ this.options.middlewares.forEach(middleware => {
64
+ this.app.use('*', middleware);
65
+ });
66
+
67
+ // Body size limit middleware (only for POST, PUT, PATCH)
68
+ this.app.use('*', async (c, next) => {
69
+ const method = c.req.method;
70
+
71
+ if (['POST', 'PUT', 'PATCH'].includes(method)) {
72
+ const contentLength = c.req.header('content-length');
73
+
74
+ if (contentLength) {
75
+ const size = parseInt(contentLength);
76
+
77
+ if (size > this.options.maxBodySize) {
78
+ const response = formatter.payloadTooLarge(size, this.options.maxBodySize);
79
+ c.header('Connection', 'close'); // Close connection for large payloads
80
+ return c.json(response, response._status);
81
+ }
82
+ }
83
+ }
84
+
85
+ await next();
86
+ });
87
+
88
+ // Kubernetes Liveness Probe - checks if app is alive
89
+ // If this fails, k8s will restart the pod
90
+ this.app.get('/health/live', (c) => {
91
+ // Simple check: if we can respond, we're alive
92
+ const response = formatter.success({
93
+ status: 'alive',
94
+ timestamp: new Date().toISOString()
95
+ });
96
+ return c.json(response);
97
+ });
98
+
99
+ // Kubernetes Readiness Probe - checks if app is ready to receive traffic
100
+ // If this fails, k8s will remove pod from service endpoints
101
+ this.app.get('/health/ready', (c) => {
102
+ // Check if database is connected and resources are loaded
103
+ const isReady = this.options.database &&
104
+ this.options.database.connected &&
105
+ Object.keys(this.options.database.resources).length > 0;
106
+
107
+ if (!isReady) {
108
+ const response = formatter.error('Service not ready', {
109
+ status: 503,
110
+ code: 'NOT_READY',
111
+ details: {
112
+ database: {
113
+ connected: this.options.database?.connected || false,
114
+ resources: Object.keys(this.options.database?.resources || {}).length
115
+ }
116
+ }
117
+ });
118
+ return c.json(response, 503);
119
+ }
120
+
121
+ const response = formatter.success({
122
+ status: 'ready',
123
+ database: {
124
+ connected: true,
125
+ resources: Object.keys(this.options.database.resources).length
126
+ },
127
+ timestamp: new Date().toISOString()
128
+ });
129
+ return c.json(response);
130
+ });
131
+
132
+ // Generic Health Check endpoint
133
+ this.app.get('/health', (c) => {
134
+ const response = formatter.success({
135
+ status: 'ok',
136
+ uptime: process.uptime(),
137
+ timestamp: new Date().toISOString(),
138
+ checks: {
139
+ liveness: '/health/live',
140
+ readiness: '/health/ready'
141
+ }
142
+ });
143
+ return c.json(response);
144
+ });
145
+
146
+ // Root endpoint - custom handler or redirect to docs
147
+ this.app.get('/', (c) => {
148
+ // If user provided a custom root handler, use it
149
+ if (this.options.rootHandler) {
150
+ return this.options.rootHandler(c);
151
+ }
152
+
153
+ // Otherwise, redirect to docs
154
+ return c.redirect('/docs', 302);
155
+ });
156
+
157
+ // OpenAPI spec endpoint
158
+ if (this.options.docsEnabled) {
159
+ this.app.get('/openapi.json', (c) => {
160
+ if (!this.openAPISpec) {
161
+ this.openAPISpec = this._generateOpenAPISpec();
162
+ }
163
+ return c.json(this.openAPISpec);
164
+ });
165
+
166
+ // API Documentation UI endpoint
167
+ if (this.options.docsUI === 'swagger') {
168
+ // Swagger UI (legacy, less pretty)
169
+ this.app.get('/docs', swaggerUI({
170
+ url: '/openapi.json'
171
+ }));
172
+ } else {
173
+ // Redoc (modern, beautiful design!)
174
+ this.app.get('/docs', (c) => {
175
+ return c.html(`<!DOCTYPE html>
176
+ <html lang="en">
177
+ <head>
178
+ <meta charset="UTF-8">
179
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
180
+ <title>${this.options.apiInfo.title} - API Documentation</title>
181
+ <style>
182
+ body {
183
+ margin: 0;
184
+ padding: 0;
185
+ }
186
+ </style>
187
+ </head>
188
+ <body>
189
+ <redoc spec-url="/openapi.json"></redoc>
190
+ <script src="https://cdn.redoc.ly/redoc/v2.5.1/bundles/redoc.standalone.js"></script>
191
+ </body>
192
+ </html>`);
193
+ });
194
+ }
195
+ }
196
+
197
+ // Setup resource routes
198
+ this._setupResourceRoutes();
199
+
200
+ // Global error handler
201
+ this.app.onError((err, c) => {
202
+ return errorHandler(err, c);
203
+ });
204
+
205
+ // 404 handler
206
+ this.app.notFound((c) => {
207
+ const response = formatter.error('Route not found', {
208
+ status: 404,
209
+ code: 'NOT_FOUND',
210
+ details: {
211
+ path: c.req.path,
212
+ method: c.req.method
213
+ }
214
+ });
215
+ return c.json(response, 404);
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Setup routes for all resources
221
+ * @private
222
+ */
223
+ _setupResourceRoutes() {
224
+ const { database, resources: resourceConfigs } = this.options;
225
+
226
+ // Get all resources from database
227
+ const resources = database.resources;
228
+
229
+ for (const [name, resource] of Object.entries(resources)) {
230
+ // Skip plugin resources unless explicitly included
231
+ if (name.startsWith('plg_') && !resourceConfigs[name]) {
232
+ continue;
233
+ }
234
+
235
+ // Get resource configuration
236
+ const config = resourceConfigs[name] || {
237
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
238
+ auth: false
239
+ };
240
+
241
+ // Determine version
242
+ const version = resource.config?.currentVersion || resource.version || 'v1';
243
+
244
+ // Create resource routes
245
+ const resourceApp = createResourceRoutes(resource, version, {
246
+ methods: config.methods,
247
+ customMiddleware: config.customMiddleware || [],
248
+ enableValidation: config.validation !== false
249
+ });
250
+
251
+ // Mount resource routes
252
+ this.app.route(`/${version}/${name}`, resourceApp);
253
+
254
+ if (this.options.verbose) {
255
+ console.log(`[API Plugin] Mounted routes for resource '${name}' at /${version}/${name}`);
256
+ }
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Start the server
262
+ * @returns {Promise<void>}
263
+ */
264
+ async start() {
265
+ if (this.isRunning) {
266
+ console.warn('[API Plugin] Server is already running');
267
+ return;
268
+ }
269
+
270
+ const { port, host } = this.options;
271
+
272
+ return new Promise((resolve, reject) => {
273
+ try {
274
+ this.server = serve({
275
+ fetch: this.app.fetch,
276
+ port,
277
+ hostname: host
278
+ }, (info) => {
279
+ this.isRunning = true;
280
+ console.log(`[API Plugin] Server listening on http://${info.address}:${info.port}`);
281
+ resolve();
282
+ });
283
+ } catch (err) {
284
+ reject(err);
285
+ }
286
+ });
287
+ }
288
+
289
+ /**
290
+ * Stop the server
291
+ * @returns {Promise<void>}
292
+ */
293
+ async stop() {
294
+ if (!this.isRunning) {
295
+ console.warn('[API Plugin] Server is not running');
296
+ return;
297
+ }
298
+
299
+ if (this.server && typeof this.server.close === 'function') {
300
+ await new Promise((resolve) => {
301
+ this.server.close(() => {
302
+ this.isRunning = false;
303
+ console.log('[API Plugin] Server stopped');
304
+ resolve();
305
+ });
306
+ });
307
+ } else {
308
+ // For some Hono adapters, server might not have close method
309
+ this.isRunning = false;
310
+ console.log('[API Plugin] Server stopped');
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Get server info
316
+ * @returns {Object} Server information
317
+ */
318
+ getInfo() {
319
+ return {
320
+ isRunning: this.isRunning,
321
+ port: this.options.port,
322
+ host: this.options.host,
323
+ resources: Object.keys(this.options.database.resources).length
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Get Hono app instance
329
+ * @returns {Hono} Hono app
330
+ */
331
+ getApp() {
332
+ return this.app;
333
+ }
334
+
335
+ /**
336
+ * Generate OpenAPI specification
337
+ * @private
338
+ * @returns {Object} OpenAPI spec
339
+ */
340
+ _generateOpenAPISpec() {
341
+ const { port, host, database, resources, auth, apiInfo } = this.options;
342
+
343
+ return generateOpenAPISpec(database, {
344
+ title: apiInfo.title,
345
+ version: apiInfo.version,
346
+ description: apiInfo.description,
347
+ serverUrl: `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`,
348
+ auth,
349
+ resources
350
+ });
351
+ }
352
+ }
353
+
354
+ export default ApiServer;