s3db.js 11.3.2 → 12.0.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.
Files changed (82) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36664 -15480
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +57 -0
  5. package/dist/s3db.es.js +36661 -15531
  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 +27 -6
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +41 -46
  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 +39 -19
  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 +539 -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 +350 -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/replicators/bigquery-replicator.class.js +180 -8
  55. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  56. package/src/plugins/replicators/index.js +28 -3
  57. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  58. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  59. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  60. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  61. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  62. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  63. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  64. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  65. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  66. package/src/plugins/state-machine.plugin.js +122 -68
  67. package/src/plugins/tfstate/README.md +745 -0
  68. package/src/plugins/tfstate/base-driver.js +80 -0
  69. package/src/plugins/tfstate/errors.js +112 -0
  70. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  71. package/src/plugins/tfstate/index.js +2660 -0
  72. package/src/plugins/tfstate/s3-driver.js +192 -0
  73. package/src/plugins/ttl.plugin.js +536 -0
  74. package/src/resource.class.js +14 -10
  75. package/src/s3db.d.ts +57 -0
  76. package/src/schema.class.js +366 -32
  77. package/SECURITY.md +0 -76
  78. package/src/partition-drivers/base-partition-driver.js +0 -106
  79. package/src/partition-drivers/index.js +0 -66
  80. package/src/partition-drivers/memory-partition-driver.js +0 -289
  81. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  82. 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,350 @@
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
+ export class ApiServer {
16
+ /**
17
+ * Create API server
18
+ * @param {Object} options - Server options
19
+ * @param {number} options.port - Server port
20
+ * @param {string} options.host - Server host
21
+ * @param {Object} options.database - s3db.js database instance
22
+ * @param {Object} options.resources - Resource configuration
23
+ * @param {Array} options.middlewares - Global middlewares
24
+ */
25
+ constructor(options = {}) {
26
+ this.options = {
27
+ port: options.port || 3000,
28
+ host: options.host || '0.0.0.0',
29
+ database: options.database,
30
+ resources: options.resources || {},
31
+ middlewares: options.middlewares || [],
32
+ verbose: options.verbose || false,
33
+ auth: options.auth || {},
34
+ docsEnabled: options.docsEnabled !== false, // Enable /docs by default
35
+ docsUI: options.docsUI || 'redoc', // 'swagger' or 'redoc'
36
+ maxBodySize: options.maxBodySize || 10 * 1024 * 1024, // 10MB default
37
+ rootHandler: options.rootHandler, // Custom handler for root path, if not provided redirects to /docs
38
+ apiInfo: {
39
+ title: options.apiTitle || 's3db.js API',
40
+ version: options.apiVersion || '1.0.0',
41
+ description: options.apiDescription || 'Auto-generated REST API for s3db.js resources'
42
+ }
43
+ };
44
+
45
+ this.app = new Hono();
46
+ this.server = null;
47
+ this.isRunning = false;
48
+ this.openAPISpec = null;
49
+
50
+ this._setupRoutes();
51
+ }
52
+
53
+ /**
54
+ * Setup all routes
55
+ * @private
56
+ */
57
+ _setupRoutes() {
58
+ // Apply global middlewares
59
+ this.options.middlewares.forEach(middleware => {
60
+ this.app.use('*', middleware);
61
+ });
62
+
63
+ // Body size limit middleware (only for POST, PUT, PATCH)
64
+ this.app.use('*', async (c, next) => {
65
+ const method = c.req.method;
66
+
67
+ if (['POST', 'PUT', 'PATCH'].includes(method)) {
68
+ const contentLength = c.req.header('content-length');
69
+
70
+ if (contentLength) {
71
+ const size = parseInt(contentLength);
72
+
73
+ if (size > this.options.maxBodySize) {
74
+ const response = formatter.payloadTooLarge(size, this.options.maxBodySize);
75
+ c.header('Connection', 'close'); // Close connection for large payloads
76
+ return c.json(response, response._status);
77
+ }
78
+ }
79
+ }
80
+
81
+ await next();
82
+ });
83
+
84
+ // Kubernetes Liveness Probe - checks if app is alive
85
+ // If this fails, k8s will restart the pod
86
+ this.app.get('/health/live', (c) => {
87
+ // Simple check: if we can respond, we're alive
88
+ const response = formatter.success({
89
+ status: 'alive',
90
+ timestamp: new Date().toISOString()
91
+ });
92
+ return c.json(response);
93
+ });
94
+
95
+ // Kubernetes Readiness Probe - checks if app is ready to receive traffic
96
+ // If this fails, k8s will remove pod from service endpoints
97
+ this.app.get('/health/ready', (c) => {
98
+ // Check if database is connected and resources are loaded
99
+ const isReady = this.options.database &&
100
+ this.options.database.connected &&
101
+ Object.keys(this.options.database.resources).length > 0;
102
+
103
+ if (!isReady) {
104
+ const response = formatter.error('Service not ready', {
105
+ status: 503,
106
+ code: 'NOT_READY',
107
+ details: {
108
+ database: {
109
+ connected: this.options.database?.connected || false,
110
+ resources: Object.keys(this.options.database?.resources || {}).length
111
+ }
112
+ }
113
+ });
114
+ return c.json(response, 503);
115
+ }
116
+
117
+ const response = formatter.success({
118
+ status: 'ready',
119
+ database: {
120
+ connected: true,
121
+ resources: Object.keys(this.options.database.resources).length
122
+ },
123
+ timestamp: new Date().toISOString()
124
+ });
125
+ return c.json(response);
126
+ });
127
+
128
+ // Generic Health Check endpoint
129
+ this.app.get('/health', (c) => {
130
+ const response = formatter.success({
131
+ status: 'ok',
132
+ uptime: process.uptime(),
133
+ timestamp: new Date().toISOString(),
134
+ checks: {
135
+ liveness: '/health/live',
136
+ readiness: '/health/ready'
137
+ }
138
+ });
139
+ return c.json(response);
140
+ });
141
+
142
+ // Root endpoint - custom handler or redirect to docs
143
+ this.app.get('/', (c) => {
144
+ // If user provided a custom root handler, use it
145
+ if (this.options.rootHandler) {
146
+ return this.options.rootHandler(c);
147
+ }
148
+
149
+ // Otherwise, redirect to docs
150
+ return c.redirect('/docs', 302);
151
+ });
152
+
153
+ // OpenAPI spec endpoint
154
+ if (this.options.docsEnabled) {
155
+ this.app.get('/openapi.json', (c) => {
156
+ if (!this.openAPISpec) {
157
+ this.openAPISpec = this._generateOpenAPISpec();
158
+ }
159
+ return c.json(this.openAPISpec);
160
+ });
161
+
162
+ // API Documentation UI endpoint
163
+ if (this.options.docsUI === 'swagger') {
164
+ // Swagger UI (legacy, less pretty)
165
+ this.app.get('/docs', swaggerUI({
166
+ url: '/openapi.json'
167
+ }));
168
+ } else {
169
+ // Redoc (modern, beautiful design!)
170
+ this.app.get('/docs', (c) => {
171
+ return c.html(`<!DOCTYPE html>
172
+ <html lang="en">
173
+ <head>
174
+ <meta charset="UTF-8">
175
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
176
+ <title>${this.options.apiInfo.title} - API Documentation</title>
177
+ <style>
178
+ body {
179
+ margin: 0;
180
+ padding: 0;
181
+ }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <redoc spec-url="/openapi.json"></redoc>
186
+ <script src="https://cdn.redoc.ly/redoc/v2.5.1/bundles/redoc.standalone.js"></script>
187
+ </body>
188
+ </html>`);
189
+ });
190
+ }
191
+ }
192
+
193
+ // Setup resource routes
194
+ this._setupResourceRoutes();
195
+
196
+ // Global error handler
197
+ this.app.onError((err, c) => {
198
+ return errorHandler(err, c);
199
+ });
200
+
201
+ // 404 handler
202
+ this.app.notFound((c) => {
203
+ const response = formatter.error('Route not found', {
204
+ status: 404,
205
+ code: 'NOT_FOUND',
206
+ details: {
207
+ path: c.req.path,
208
+ method: c.req.method
209
+ }
210
+ });
211
+ return c.json(response, 404);
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Setup routes for all resources
217
+ * @private
218
+ */
219
+ _setupResourceRoutes() {
220
+ const { database, resources: resourceConfigs } = this.options;
221
+
222
+ // Get all resources from database
223
+ const resources = database.resources;
224
+
225
+ for (const [name, resource] of Object.entries(resources)) {
226
+ // Skip plugin resources unless explicitly included
227
+ if (name.startsWith('plg_') && !resourceConfigs[name]) {
228
+ continue;
229
+ }
230
+
231
+ // Get resource configuration
232
+ const config = resourceConfigs[name] || {
233
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
234
+ auth: false
235
+ };
236
+
237
+ // Determine version
238
+ const version = resource.config?.currentVersion || resource.version || 'v1';
239
+
240
+ // Create resource routes
241
+ const resourceApp = createResourceRoutes(resource, version, {
242
+ methods: config.methods,
243
+ customMiddleware: config.customMiddleware || [],
244
+ enableValidation: config.validation !== false
245
+ });
246
+
247
+ // Mount resource routes
248
+ this.app.route(`/${version}/${name}`, resourceApp);
249
+
250
+ if (this.options.verbose) {
251
+ console.log(`[API Plugin] Mounted routes for resource '${name}' at /${version}/${name}`);
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Start the server
258
+ * @returns {Promise<void>}
259
+ */
260
+ async start() {
261
+ if (this.isRunning) {
262
+ console.warn('[API Plugin] Server is already running');
263
+ return;
264
+ }
265
+
266
+ const { port, host } = this.options;
267
+
268
+ return new Promise((resolve, reject) => {
269
+ try {
270
+ this.server = serve({
271
+ fetch: this.app.fetch,
272
+ port,
273
+ hostname: host
274
+ }, (info) => {
275
+ this.isRunning = true;
276
+ console.log(`[API Plugin] Server listening on http://${info.address}:${info.port}`);
277
+ resolve();
278
+ });
279
+ } catch (err) {
280
+ reject(err);
281
+ }
282
+ });
283
+ }
284
+
285
+ /**
286
+ * Stop the server
287
+ * @returns {Promise<void>}
288
+ */
289
+ async stop() {
290
+ if (!this.isRunning) {
291
+ console.warn('[API Plugin] Server is not running');
292
+ return;
293
+ }
294
+
295
+ if (this.server && typeof this.server.close === 'function') {
296
+ await new Promise((resolve) => {
297
+ this.server.close(() => {
298
+ this.isRunning = false;
299
+ console.log('[API Plugin] Server stopped');
300
+ resolve();
301
+ });
302
+ });
303
+ } else {
304
+ // For some Hono adapters, server might not have close method
305
+ this.isRunning = false;
306
+ console.log('[API Plugin] Server stopped');
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Get server info
312
+ * @returns {Object} Server information
313
+ */
314
+ getInfo() {
315
+ return {
316
+ isRunning: this.isRunning,
317
+ port: this.options.port,
318
+ host: this.options.host,
319
+ resources: Object.keys(this.options.database.resources).length
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Get Hono app instance
325
+ * @returns {Hono} Hono app
326
+ */
327
+ getApp() {
328
+ return this.app;
329
+ }
330
+
331
+ /**
332
+ * Generate OpenAPI specification
333
+ * @private
334
+ * @returns {Object} OpenAPI spec
335
+ */
336
+ _generateOpenAPISpec() {
337
+ const { port, host, database, resources, auth, apiInfo } = this.options;
338
+
339
+ return generateOpenAPISpec(database, {
340
+ title: apiInfo.title,
341
+ version: apiInfo.version,
342
+ description: apiInfo.description,
343
+ serverUrl: `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`,
344
+ auth,
345
+ resources
346
+ });
347
+ }
348
+ }
349
+
350
+ export default ApiServer;