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,1240 @@
1
+ /**
2
+ * OpenAPI Generator - Generate OpenAPI 3.1 specification from s3db.js resources
3
+ *
4
+ * Automatically creates OpenAPI documentation based on resource schemas
5
+ * Note: OpenAPI 3.2.0 is not yet supported by Redoc v2.5.1
6
+ */
7
+
8
+ /**
9
+ * Map s3db.js field types to OpenAPI types
10
+ * @param {string} fieldType - s3db.js field type
11
+ * @returns {Object} OpenAPI type definition
12
+ */
13
+ function mapFieldTypeToOpenAPI(fieldType) {
14
+ const type = fieldType.split('|')[0].trim();
15
+
16
+ const typeMap = {
17
+ 'string': { type: 'string' },
18
+ 'number': { type: 'number' },
19
+ 'integer': { type: 'integer' },
20
+ 'boolean': { type: 'boolean' },
21
+ 'array': { type: 'array', items: { type: 'string' } },
22
+ 'object': { type: 'object' },
23
+ 'json': { type: 'object' },
24
+ 'secret': { type: 'string', format: 'password' },
25
+ 'email': { type: 'string', format: 'email' },
26
+ 'url': { type: 'string', format: 'uri' },
27
+ 'date': { type: 'string', format: 'date' },
28
+ 'datetime': { type: 'string', format: 'date-time' },
29
+ 'ip4': { type: 'string', format: 'ipv4', description: 'IPv4 address' },
30
+ 'ip6': { type: 'string', format: 'ipv6', description: 'IPv6 address' },
31
+ 'embedding': { type: 'array', items: { type: 'number' }, description: 'Vector embedding' }
32
+ };
33
+
34
+ // Handle embedding:N notation
35
+ if (type.startsWith('embedding:')) {
36
+ const length = parseInt(type.split(':')[1]);
37
+ return {
38
+ type: 'array',
39
+ items: { type: 'number' },
40
+ minItems: length,
41
+ maxItems: length,
42
+ description: `Vector embedding (${length} dimensions)`
43
+ };
44
+ }
45
+
46
+ return typeMap[type] || { type: 'string' };
47
+ }
48
+
49
+ /**
50
+ * Extract validation rules from field definition
51
+ * @param {string} fieldDef - Field definition string
52
+ * @returns {Object} Validation rules
53
+ */
54
+ function extractValidationRules(fieldDef) {
55
+ const rules = {};
56
+ const parts = fieldDef.split('|');
57
+
58
+ for (const part of parts) {
59
+ const [rule, value] = part.split(':').map(s => s.trim());
60
+
61
+ switch (rule) {
62
+ case 'required':
63
+ rules.required = true;
64
+ break;
65
+ case 'min':
66
+ rules.minimum = parseFloat(value);
67
+ break;
68
+ case 'max':
69
+ rules.maximum = parseFloat(value);
70
+ break;
71
+ case 'minlength':
72
+ rules.minLength = parseInt(value);
73
+ break;
74
+ case 'maxlength':
75
+ rules.maxLength = parseInt(value);
76
+ break;
77
+ case 'pattern':
78
+ rules.pattern = value;
79
+ break;
80
+ case 'enum':
81
+ rules.enum = value.split(',').map(v => v.trim());
82
+ break;
83
+ case 'default':
84
+ rules.default = value;
85
+ break;
86
+ }
87
+ }
88
+
89
+ return rules;
90
+ }
91
+
92
+ /**
93
+ * Generate OpenAPI schema for a resource
94
+ * @param {Object} resource - s3db.js Resource instance
95
+ * @returns {Object} OpenAPI schema definition
96
+ */
97
+ function generateResourceSchema(resource) {
98
+ const properties = {};
99
+ const required = [];
100
+
101
+ const attributes = resource.config?.attributes || resource.attributes || {};
102
+
103
+ // Add system-generated id field (always present in responses)
104
+ properties.id = {
105
+ type: 'string',
106
+ description: 'Unique identifier for the resource',
107
+ example: '2_gDTpeU6EI0e8B92n_R3Y',
108
+ readOnly: true
109
+ };
110
+
111
+ for (const [fieldName, fieldDef] of Object.entries(attributes)) {
112
+ // Handle object notation
113
+ if (typeof fieldDef === 'object' && fieldDef.type) {
114
+ const baseType = mapFieldTypeToOpenAPI(fieldDef.type);
115
+ properties[fieldName] = {
116
+ ...baseType,
117
+ description: fieldDef.description || undefined
118
+ };
119
+
120
+ if (fieldDef.required) {
121
+ required.push(fieldName);
122
+ }
123
+
124
+ // Handle nested object properties
125
+ if (fieldDef.type === 'object' && fieldDef.props) {
126
+ properties[fieldName].properties = {};
127
+ for (const [propName, propDef] of Object.entries(fieldDef.props)) {
128
+ const propType = typeof propDef === 'string' ? propDef : propDef.type;
129
+ properties[fieldName].properties[propName] = mapFieldTypeToOpenAPI(propType);
130
+ }
131
+ }
132
+
133
+ // Handle array items
134
+ if (fieldDef.type === 'array' && fieldDef.items) {
135
+ properties[fieldName].items = mapFieldTypeToOpenAPI(fieldDef.items);
136
+ }
137
+ }
138
+ // Handle string notation
139
+ else if (typeof fieldDef === 'string') {
140
+ const baseType = mapFieldTypeToOpenAPI(fieldDef);
141
+ const rules = extractValidationRules(fieldDef);
142
+
143
+ properties[fieldName] = {
144
+ ...baseType,
145
+ ...rules
146
+ };
147
+
148
+ if (rules.required) {
149
+ required.push(fieldName);
150
+ delete properties[fieldName].required; // Move to schema-level required array
151
+ }
152
+ }
153
+ }
154
+
155
+ return {
156
+ type: 'object',
157
+ properties,
158
+ required: required.length > 0 ? required : undefined
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Generate OpenAPI paths for a resource
164
+ * @param {Object} resource - s3db.js Resource instance
165
+ * @param {string} version - Resource version
166
+ * @param {Object} config - Resource configuration
167
+ * @returns {Object} OpenAPI paths
168
+ */
169
+ function generateResourcePaths(resource, version, config = {}) {
170
+ const resourceName = resource.name;
171
+ const basePath = `/${version}/${resourceName}`;
172
+ const schema = generateResourceSchema(resource);
173
+ const methods = config.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
174
+ const authMethods = config.auth || [];
175
+ const requiresAuth = authMethods && authMethods.length > 0;
176
+
177
+ const paths = {};
178
+
179
+ // Security schemes
180
+ const security = [];
181
+ if (requiresAuth) {
182
+ if (authMethods.includes('jwt')) security.push({ bearerAuth: [] });
183
+ if (authMethods.includes('apiKey')) security.push({ apiKeyAuth: [] });
184
+ if (authMethods.includes('basic')) security.push({ basicAuth: [] });
185
+ }
186
+
187
+ // Extract partition information for documentation
188
+ // Partitions are stored in resource.config.options.partitions
189
+ const partitions = resource.config?.options?.partitions || resource.config?.partitions || resource.partitions || {};
190
+ const partitionNames = Object.keys(partitions);
191
+ const hasPartitions = partitionNames.length > 0;
192
+
193
+ // Build partition documentation
194
+ let partitionDescription = 'Partition name for filtering';
195
+ let partitionValuesDescription = 'Partition values as JSON string';
196
+ let partitionExample = undefined;
197
+ let partitionValuesExample = undefined;
198
+
199
+ if (hasPartitions) {
200
+ // Build detailed partition description
201
+ const partitionDocs = partitionNames.map(name => {
202
+ const partition = partitions[name];
203
+ const fields = Object.keys(partition.fields || {});
204
+ const fieldTypes = Object.entries(partition.fields || {})
205
+ .map(([field, type]) => `${field}: ${type}`)
206
+ .join(', ');
207
+ return `- **${name}**: Filters by ${fields.join(', ')} (${fieldTypes})`;
208
+ }).join('\n');
209
+
210
+ partitionDescription = `Available partitions:\n${partitionDocs}`;
211
+
212
+ // Build partition values description with examples
213
+ const examplePartition = partitionNames[0];
214
+ const exampleFields = partitions[examplePartition]?.fields || {};
215
+ const exampleFieldsDoc = Object.entries(exampleFields)
216
+ .map(([field, type]) => `"${field}": <${type} value>`)
217
+ .join(', ');
218
+
219
+ partitionValuesDescription = `Partition field values as JSON string. Must match the structure of the selected partition.\n\nExample for "${examplePartition}" partition: \`{"${Object.keys(exampleFields)[0]}": "value"}\``;
220
+
221
+ // Set examples
222
+ partitionExample = examplePartition;
223
+ const firstField = Object.keys(exampleFields)[0];
224
+ const firstFieldType = exampleFields[firstField];
225
+ let exampleValue = 'example';
226
+ if (firstFieldType === 'number' || firstFieldType === 'integer') {
227
+ exampleValue = 123;
228
+ } else if (firstFieldType === 'boolean') {
229
+ exampleValue = true;
230
+ }
231
+ partitionValuesExample = JSON.stringify({ [firstField]: exampleValue });
232
+ }
233
+
234
+ // Extract partition fields for query parameters (filtering)
235
+ // Only fields that are part of partitions can be efficiently filtered
236
+ const attributeQueryParams = [];
237
+
238
+ if (hasPartitions) {
239
+ const partitionFieldsSet = new Set();
240
+
241
+ // Collect all unique fields from all partitions
242
+ for (const [partitionName, partition] of Object.entries(partitions)) {
243
+ const fields = partition.fields || {};
244
+ for (const fieldName of Object.keys(fields)) {
245
+ partitionFieldsSet.add(fieldName);
246
+ }
247
+ }
248
+
249
+ // Create query parameters only for partition fields
250
+ const attributes = resource.config?.attributes || resource.attributes || {};
251
+
252
+ for (const fieldName of partitionFieldsSet) {
253
+ const fieldDef = attributes[fieldName];
254
+ if (!fieldDef) continue; // Skip if field doesn't exist in schema
255
+
256
+ // Get field type
257
+ let fieldType;
258
+ if (typeof fieldDef === 'object' && fieldDef.type) {
259
+ fieldType = fieldDef.type;
260
+ } else if (typeof fieldDef === 'string') {
261
+ fieldType = fieldDef.split('|')[0].trim();
262
+ } else {
263
+ fieldType = 'string';
264
+ }
265
+
266
+ // Map to OpenAPI type
267
+ const openAPIType = mapFieldTypeToOpenAPI(fieldType);
268
+
269
+ // Create query parameter with partition info
270
+ attributeQueryParams.push({
271
+ name: fieldName,
272
+ in: 'query',
273
+ description: `Filter by ${fieldName} field (indexed via partitions for efficient querying). Value will be parsed as JSON if possible, otherwise treated as string.`,
274
+ required: false,
275
+ schema: openAPIType
276
+ });
277
+ }
278
+ }
279
+
280
+ // List endpoint with filtering support
281
+ if (methods.includes('GET')) {
282
+ paths[basePath] = {
283
+ get: {
284
+ tags: [resourceName],
285
+ summary: `List ${resourceName}`,
286
+ description: `Retrieve a paginated list of ${resourceName}. Supports filtering by passing any resource field as a query parameter (e.g., ?status=active&year=2024). Values are parsed as JSON if possible, otherwise treated as strings.
287
+
288
+ **Pagination**: Use \`limit\` and \`offset\` to paginate results. For example:
289
+ - First page (10 items): \`?limit=10&offset=0\`
290
+ - Second page: \`?limit=10&offset=10\`
291
+ - Third page: \`?limit=10&offset=20\`
292
+
293
+ The response includes pagination metadata in the \`pagination\` object with total count and page information.${hasPartitions ? '\n\n**Partitioning**: This resource supports partitioned queries for optimized filtering. Use the `partition` and `partitionValues` parameters together.' : ''}`,
294
+ parameters: [
295
+ {
296
+ name: 'limit',
297
+ in: 'query',
298
+ description: 'Maximum number of items to return per page (page size)',
299
+ schema: { type: 'integer', default: 100, minimum: 1, maximum: 1000 },
300
+ example: 10
301
+ },
302
+ {
303
+ name: 'offset',
304
+ in: 'query',
305
+ description: 'Number of items to skip before starting to return results. Use for pagination: offset = (page - 1) * limit',
306
+ schema: { type: 'integer', default: 0, minimum: 0 },
307
+ example: 0
308
+ },
309
+ ...(hasPartitions ? [
310
+ {
311
+ name: 'partition',
312
+ in: 'query',
313
+ description: partitionDescription,
314
+ schema: {
315
+ type: 'string',
316
+ enum: partitionNames
317
+ },
318
+ example: partitionExample
319
+ },
320
+ {
321
+ name: 'partitionValues',
322
+ in: 'query',
323
+ description: partitionValuesDescription,
324
+ schema: { type: 'string' },
325
+ example: partitionValuesExample
326
+ }
327
+ ] : []),
328
+ ...attributeQueryParams
329
+ ],
330
+ responses: {
331
+ 200: {
332
+ description: 'Successful response',
333
+ content: {
334
+ 'application/json': {
335
+ schema: {
336
+ type: 'object',
337
+ properties: {
338
+ success: { type: 'boolean', example: true },
339
+ data: {
340
+ type: 'array',
341
+ items: schema
342
+ },
343
+ pagination: {
344
+ type: 'object',
345
+ description: 'Pagination metadata for the current request',
346
+ properties: {
347
+ total: {
348
+ type: 'integer',
349
+ description: 'Total number of items available',
350
+ example: 150
351
+ },
352
+ page: {
353
+ type: 'integer',
354
+ description: 'Current page number (1-indexed)',
355
+ example: 1
356
+ },
357
+ pageSize: {
358
+ type: 'integer',
359
+ description: 'Number of items per page (same as limit parameter)',
360
+ example: 10
361
+ },
362
+ pageCount: {
363
+ type: 'integer',
364
+ description: 'Total number of pages available',
365
+ example: 15
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ },
373
+ headers: {
374
+ 'X-Total-Count': {
375
+ description: 'Total number of records',
376
+ schema: { type: 'integer' }
377
+ },
378
+ 'X-Page-Count': {
379
+ description: 'Total number of pages',
380
+ schema: { type: 'integer' }
381
+ }
382
+ }
383
+ }
384
+ },
385
+ security: security.length > 0 ? security : undefined
386
+ }
387
+ };
388
+ }
389
+
390
+ // Get by ID endpoint
391
+ if (methods.includes('GET')) {
392
+ paths[`${basePath}/{id}`] = {
393
+ get: {
394
+ tags: [resourceName],
395
+ summary: `Get ${resourceName} by ID`,
396
+ description: `Retrieve a single ${resourceName} by its ID${hasPartitions ? '. Optionally specify a partition for more efficient retrieval.' : ''}`,
397
+ parameters: [
398
+ {
399
+ name: 'id',
400
+ in: 'path',
401
+ required: true,
402
+ description: `${resourceName} ID`,
403
+ schema: { type: 'string' }
404
+ },
405
+ ...(hasPartitions ? [
406
+ {
407
+ name: 'partition',
408
+ in: 'query',
409
+ description: partitionDescription,
410
+ schema: {
411
+ type: 'string',
412
+ enum: partitionNames
413
+ },
414
+ example: partitionExample
415
+ },
416
+ {
417
+ name: 'partitionValues',
418
+ in: 'query',
419
+ description: partitionValuesDescription,
420
+ schema: { type: 'string' },
421
+ example: partitionValuesExample
422
+ }
423
+ ] : [])
424
+ ],
425
+ responses: {
426
+ 200: {
427
+ description: 'Successful response',
428
+ content: {
429
+ 'application/json': {
430
+ schema: {
431
+ type: 'object',
432
+ properties: {
433
+ success: { type: 'boolean', example: true },
434
+ data: schema
435
+ }
436
+ }
437
+ }
438
+ }
439
+ },
440
+ 404: {
441
+ description: 'Resource not found',
442
+ content: {
443
+ 'application/json': {
444
+ schema: { $ref: '#/components/schemas/Error' }
445
+ }
446
+ }
447
+ }
448
+ },
449
+ security: security.length > 0 ? security : undefined
450
+ }
451
+ };
452
+ }
453
+
454
+ // Create endpoint
455
+ if (methods.includes('POST')) {
456
+ if (!paths[basePath]) paths[basePath] = {};
457
+ paths[basePath].post = {
458
+ tags: [resourceName],
459
+ summary: `Create ${resourceName}`,
460
+ description: `Create a new ${resourceName}`,
461
+ requestBody: {
462
+ required: true,
463
+ content: {
464
+ 'application/json': {
465
+ schema: schema
466
+ }
467
+ }
468
+ },
469
+ responses: {
470
+ 201: {
471
+ description: 'Resource created successfully',
472
+ content: {
473
+ 'application/json': {
474
+ schema: {
475
+ type: 'object',
476
+ properties: {
477
+ success: { type: 'boolean', example: true },
478
+ data: schema
479
+ }
480
+ }
481
+ }
482
+ },
483
+ headers: {
484
+ Location: {
485
+ description: 'URL of the created resource',
486
+ schema: { type: 'string' }
487
+ }
488
+ }
489
+ },
490
+ 400: {
491
+ description: 'Validation error',
492
+ content: {
493
+ 'application/json': {
494
+ schema: { $ref: '#/components/schemas/ValidationError' }
495
+ }
496
+ }
497
+ }
498
+ },
499
+ security: security.length > 0 ? security : undefined
500
+ };
501
+ }
502
+
503
+ // Update (full) endpoint
504
+ if (methods.includes('PUT')) {
505
+ if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
506
+ paths[`${basePath}/{id}`].put = {
507
+ tags: [resourceName],
508
+ summary: `Update ${resourceName} (full)`,
509
+ description: `Fully update a ${resourceName}`,
510
+ parameters: [
511
+ {
512
+ name: 'id',
513
+ in: 'path',
514
+ required: true,
515
+ schema: { type: 'string' }
516
+ }
517
+ ],
518
+ requestBody: {
519
+ required: true,
520
+ content: {
521
+ 'application/json': {
522
+ schema: schema
523
+ }
524
+ }
525
+ },
526
+ responses: {
527
+ 200: {
528
+ description: 'Resource updated successfully',
529
+ content: {
530
+ 'application/json': {
531
+ schema: {
532
+ type: 'object',
533
+ properties: {
534
+ success: { type: 'boolean', example: true },
535
+ data: schema
536
+ }
537
+ }
538
+ }
539
+ }
540
+ },
541
+ 404: {
542
+ description: 'Resource not found',
543
+ content: {
544
+ 'application/json': {
545
+ schema: { $ref: '#/components/schemas/Error' }
546
+ }
547
+ }
548
+ }
549
+ },
550
+ security: security.length > 0 ? security : undefined
551
+ };
552
+ }
553
+
554
+ // Update (partial) endpoint
555
+ if (methods.includes('PATCH')) {
556
+ if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
557
+ paths[`${basePath}/{id}`].patch = {
558
+ tags: [resourceName],
559
+ summary: `Update ${resourceName} (partial)`,
560
+ description: `Partially update a ${resourceName}`,
561
+ parameters: [
562
+ {
563
+ name: 'id',
564
+ in: 'path',
565
+ required: true,
566
+ schema: { type: 'string' }
567
+ }
568
+ ],
569
+ requestBody: {
570
+ required: true,
571
+ content: {
572
+ 'application/json': {
573
+ schema: {
574
+ ...schema,
575
+ required: undefined // Partial updates don't require all fields
576
+ }
577
+ }
578
+ }
579
+ },
580
+ responses: {
581
+ 200: {
582
+ description: 'Resource updated successfully',
583
+ content: {
584
+ 'application/json': {
585
+ schema: {
586
+ type: 'object',
587
+ properties: {
588
+ success: { type: 'boolean', example: true },
589
+ data: schema
590
+ }
591
+ }
592
+ }
593
+ }
594
+ },
595
+ 404: {
596
+ description: 'Resource not found',
597
+ content: {
598
+ 'application/json': {
599
+ schema: { $ref: '#/components/schemas/Error' }
600
+ }
601
+ }
602
+ }
603
+ },
604
+ security: security.length > 0 ? security : undefined
605
+ };
606
+ }
607
+
608
+ // Delete endpoint
609
+ if (methods.includes('DELETE')) {
610
+ if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
611
+ paths[`${basePath}/{id}`].delete = {
612
+ tags: [resourceName],
613
+ summary: `Delete ${resourceName}`,
614
+ description: `Delete a ${resourceName} by ID`,
615
+ parameters: [
616
+ {
617
+ name: 'id',
618
+ in: 'path',
619
+ required: true,
620
+ schema: { type: 'string' }
621
+ }
622
+ ],
623
+ responses: {
624
+ 204: {
625
+ description: 'Resource deleted successfully'
626
+ },
627
+ 404: {
628
+ description: 'Resource not found',
629
+ content: {
630
+ 'application/json': {
631
+ schema: { $ref: '#/components/schemas/Error' }
632
+ }
633
+ }
634
+ }
635
+ },
636
+ security: security.length > 0 ? security : undefined
637
+ };
638
+ }
639
+
640
+ // HEAD endpoint - Get resource statistics
641
+ if (methods.includes('HEAD')) {
642
+ if (!paths[basePath]) paths[basePath] = {};
643
+ paths[basePath].head = {
644
+ tags: [resourceName],
645
+ summary: `Get ${resourceName} statistics`,
646
+ description: `Get statistics about ${resourceName} collection without retrieving data. Returns statistics in response headers.`,
647
+ responses: {
648
+ 200: {
649
+ description: 'Statistics retrieved successfully',
650
+ headers: {
651
+ 'X-Total-Count': {
652
+ description: 'Total number of records',
653
+ schema: { type: 'integer' }
654
+ },
655
+ 'X-Resource-Version': {
656
+ description: 'Current resource version',
657
+ schema: { type: 'string' }
658
+ },
659
+ 'X-Schema-Fields': {
660
+ description: 'Number of schema fields',
661
+ schema: { type: 'integer' }
662
+ }
663
+ }
664
+ }
665
+ },
666
+ security: security.length > 0 ? security : undefined
667
+ };
668
+
669
+ // HEAD for individual resource
670
+ if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
671
+ paths[`${basePath}/{id}`].head = {
672
+ tags: [resourceName],
673
+ summary: `Check if ${resourceName} exists`,
674
+ description: `Check if a ${resourceName} exists without retrieving its data`,
675
+ parameters: [
676
+ {
677
+ name: 'id',
678
+ in: 'path',
679
+ required: true,
680
+ schema: { type: 'string' }
681
+ }
682
+ ],
683
+ responses: {
684
+ 200: {
685
+ description: 'Resource exists',
686
+ headers: {
687
+ 'Last-Modified': {
688
+ description: 'Last modification date',
689
+ schema: { type: 'string', format: 'date-time' }
690
+ }
691
+ }
692
+ },
693
+ 404: {
694
+ description: 'Resource not found'
695
+ }
696
+ },
697
+ security: security.length > 0 ? security : undefined
698
+ };
699
+ }
700
+
701
+ // OPTIONS endpoint - Get resource metadata
702
+ if (methods.includes('OPTIONS')) {
703
+ if (!paths[basePath]) paths[basePath] = {};
704
+ paths[basePath].options = {
705
+ tags: [resourceName],
706
+ summary: `Get ${resourceName} metadata`,
707
+ description: `Get complete metadata about ${resourceName} resource including schema, allowed methods, endpoints, and query parameters`,
708
+ responses: {
709
+ 200: {
710
+ description: 'Metadata retrieved successfully',
711
+ headers: {
712
+ 'Allow': {
713
+ description: 'Allowed HTTP methods',
714
+ schema: { type: 'string', example: 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS' }
715
+ }
716
+ },
717
+ content: {
718
+ 'application/json': {
719
+ schema: {
720
+ type: 'object',
721
+ properties: {
722
+ resource: { type: 'string' },
723
+ version: { type: 'string' },
724
+ totalRecords: { type: 'integer' },
725
+ allowedMethods: {
726
+ type: 'array',
727
+ items: { type: 'string' }
728
+ },
729
+ schema: {
730
+ type: 'array',
731
+ items: {
732
+ type: 'object',
733
+ properties: {
734
+ name: { type: 'string' },
735
+ type: { type: 'string' },
736
+ rules: { type: 'array', items: { type: 'string' } }
737
+ }
738
+ }
739
+ },
740
+ endpoints: {
741
+ type: 'object',
742
+ properties: {
743
+ list: { type: 'string' },
744
+ get: { type: 'string' },
745
+ create: { type: 'string' },
746
+ update: { type: 'string' },
747
+ delete: { type: 'string' }
748
+ }
749
+ },
750
+ queryParameters: { type: 'object' }
751
+ }
752
+ }
753
+ }
754
+ }
755
+ }
756
+ }
757
+ };
758
+
759
+ // OPTIONS for individual resource
760
+ if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
761
+ paths[`${basePath}/{id}`].options = {
762
+ tags: [resourceName],
763
+ summary: `Get allowed methods for ${resourceName} item`,
764
+ description: `Get allowed HTTP methods for individual ${resourceName} operations`,
765
+ parameters: [
766
+ {
767
+ name: 'id',
768
+ in: 'path',
769
+ required: true,
770
+ schema: { type: 'string' }
771
+ }
772
+ ],
773
+ responses: {
774
+ 204: {
775
+ description: 'Methods retrieved successfully',
776
+ headers: {
777
+ 'Allow': {
778
+ description: 'Allowed HTTP methods',
779
+ schema: { type: 'string', example: 'GET, PUT, PATCH, DELETE, HEAD, OPTIONS' }
780
+ }
781
+ }
782
+ }
783
+ }
784
+ };
785
+ }
786
+
787
+ return paths;
788
+ }
789
+
790
+ /**
791
+ * Generate complete OpenAPI 3.0 specification
792
+ * @param {Object} database - s3db.js Database instance
793
+ * @param {Object} config - API configuration
794
+ * @returns {Object} OpenAPI 3.0 specification
795
+ */
796
+ export function generateOpenAPISpec(database, config = {}) {
797
+ const {
798
+ title = 's3db.js API',
799
+ version = '1.0.0',
800
+ description = 'Auto-generated REST API documentation for s3db.js resources',
801
+ serverUrl = 'http://localhost:3000',
802
+ auth = {},
803
+ resources: resourceConfigs = {}
804
+ } = config;
805
+
806
+ const spec = {
807
+ openapi: '3.1.0',
808
+ info: {
809
+ title,
810
+ version,
811
+ description,
812
+ contact: {
813
+ name: 's3db.js',
814
+ url: 'https://github.com/forattini-dev/s3db.js'
815
+ }
816
+ },
817
+ servers: [
818
+ {
819
+ url: serverUrl,
820
+ description: 'API Server'
821
+ }
822
+ ],
823
+ paths: {},
824
+ components: {
825
+ schemas: {
826
+ Error: {
827
+ type: 'object',
828
+ properties: {
829
+ success: { type: 'boolean', example: false },
830
+ error: {
831
+ type: 'object',
832
+ properties: {
833
+ message: { type: 'string' },
834
+ code: { type: 'string' },
835
+ details: { type: 'object' }
836
+ }
837
+ }
838
+ }
839
+ },
840
+ ValidationError: {
841
+ type: 'object',
842
+ properties: {
843
+ success: { type: 'boolean', example: false },
844
+ error: {
845
+ type: 'object',
846
+ properties: {
847
+ message: { type: 'string', example: 'Validation failed' },
848
+ code: { type: 'string', example: 'VALIDATION_ERROR' },
849
+ details: {
850
+ type: 'object',
851
+ properties: {
852
+ errors: {
853
+ type: 'array',
854
+ items: {
855
+ type: 'object',
856
+ properties: {
857
+ field: { type: 'string' },
858
+ message: { type: 'string' },
859
+ expected: { type: 'string' },
860
+ actual: {}
861
+ }
862
+ }
863
+ }
864
+ }
865
+ }
866
+ }
867
+ }
868
+ }
869
+ }
870
+ },
871
+ securitySchemes: {}
872
+ },
873
+ tags: []
874
+ };
875
+
876
+ // Add security schemes
877
+ if (auth.jwt?.enabled) {
878
+ spec.components.securitySchemes.bearerAuth = {
879
+ type: 'http',
880
+ scheme: 'bearer',
881
+ bearerFormat: 'JWT',
882
+ description: 'JWT authentication'
883
+ };
884
+ }
885
+
886
+ if (auth.apiKey?.enabled) {
887
+ spec.components.securitySchemes.apiKeyAuth = {
888
+ type: 'apiKey',
889
+ in: 'header',
890
+ name: auth.apiKey.headerName || 'X-API-Key',
891
+ description: 'API Key authentication'
892
+ };
893
+ }
894
+
895
+ if (auth.basic?.enabled) {
896
+ spec.components.securitySchemes.basicAuth = {
897
+ type: 'http',
898
+ scheme: 'basic',
899
+ description: 'HTTP Basic authentication'
900
+ };
901
+ }
902
+
903
+ // Generate paths for each resource
904
+ const resources = database.resources;
905
+
906
+ for (const [name, resource] of Object.entries(resources)) {
907
+ // Skip plugin resources unless explicitly configured
908
+ if (name.startsWith('plg_') && !resourceConfigs[name]) {
909
+ continue;
910
+ }
911
+
912
+ // Get resource configuration
913
+ const config = resourceConfigs[name] || {
914
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
915
+ auth: false
916
+ };
917
+
918
+ // Determine version
919
+ const version = resource.config?.currentVersion || resource.version || 'v1';
920
+
921
+ // Generate paths
922
+ const paths = generateResourcePaths(resource, version, config);
923
+
924
+ // Merge paths
925
+ Object.assign(spec.paths, paths);
926
+
927
+ // Add tag
928
+ spec.tags.push({
929
+ name: name,
930
+ description: `Operations for ${name} resource`
931
+ });
932
+
933
+ // Add schema to components
934
+ spec.components.schemas[name] = generateResourceSchema(resource);
935
+ }
936
+
937
+ // Add authentication endpoints if enabled
938
+ if (auth.jwt?.enabled || auth.apiKey?.enabled || auth.basic?.enabled) {
939
+ spec.paths['/auth/login'] = {
940
+ post: {
941
+ tags: ['Authentication'],
942
+ summary: 'Login',
943
+ description: 'Authenticate with username and password',
944
+ requestBody: {
945
+ required: true,
946
+ content: {
947
+ 'application/json': {
948
+ schema: {
949
+ type: 'object',
950
+ properties: {
951
+ username: { type: 'string' },
952
+ password: { type: 'string', format: 'password' }
953
+ },
954
+ required: ['username', 'password']
955
+ }
956
+ }
957
+ }
958
+ },
959
+ responses: {
960
+ 200: {
961
+ description: 'Login successful',
962
+ content: {
963
+ 'application/json': {
964
+ schema: {
965
+ type: 'object',
966
+ properties: {
967
+ success: { type: 'boolean', example: true },
968
+ data: {
969
+ type: 'object',
970
+ properties: {
971
+ token: { type: 'string' },
972
+ user: { type: 'object' }
973
+ }
974
+ }
975
+ }
976
+ }
977
+ }
978
+ }
979
+ },
980
+ 401: {
981
+ description: 'Invalid credentials',
982
+ content: {
983
+ 'application/json': {
984
+ schema: { $ref: '#/components/schemas/Error' }
985
+ }
986
+ }
987
+ }
988
+ }
989
+ }
990
+ };
991
+
992
+ spec.paths['/auth/register'] = {
993
+ post: {
994
+ tags: ['Authentication'],
995
+ summary: 'Register',
996
+ description: 'Register a new user',
997
+ requestBody: {
998
+ required: true,
999
+ content: {
1000
+ 'application/json': {
1001
+ schema: {
1002
+ type: 'object',
1003
+ properties: {
1004
+ username: { type: 'string', minLength: 3 },
1005
+ password: { type: 'string', format: 'password', minLength: 8 },
1006
+ email: { type: 'string', format: 'email' }
1007
+ },
1008
+ required: ['username', 'password']
1009
+ }
1010
+ }
1011
+ }
1012
+ },
1013
+ responses: {
1014
+ 201: {
1015
+ description: 'User registered successfully',
1016
+ content: {
1017
+ 'application/json': {
1018
+ schema: {
1019
+ type: 'object',
1020
+ properties: {
1021
+ success: { type: 'boolean', example: true },
1022
+ data: {
1023
+ type: 'object',
1024
+ properties: {
1025
+ token: { type: 'string' },
1026
+ user: { type: 'object' }
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+ }
1036
+ };
1037
+
1038
+ spec.tags.push({
1039
+ name: 'Authentication',
1040
+ description: 'Authentication endpoints'
1041
+ });
1042
+ }
1043
+
1044
+ // Add health endpoints for Kubernetes probes
1045
+ spec.paths['/health'] = {
1046
+ get: {
1047
+ tags: ['Health'],
1048
+ summary: 'Generic Health Check',
1049
+ description: 'Generic health check endpoint that includes references to liveness and readiness probes',
1050
+ responses: {
1051
+ 200: {
1052
+ description: 'API is healthy',
1053
+ content: {
1054
+ 'application/json': {
1055
+ schema: {
1056
+ type: 'object',
1057
+ properties: {
1058
+ success: { type: 'boolean', example: true },
1059
+ data: {
1060
+ type: 'object',
1061
+ properties: {
1062
+ status: { type: 'string', example: 'ok' },
1063
+ uptime: { type: 'number', description: 'Process uptime in seconds' },
1064
+ timestamp: { type: 'string', format: 'date-time' },
1065
+ checks: {
1066
+ type: 'object',
1067
+ properties: {
1068
+ liveness: { type: 'string', example: '/health/live' },
1069
+ readiness: { type: 'string', example: '/health/ready' }
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ }
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ };
1082
+
1083
+ spec.paths['/health/live'] = {
1084
+ get: {
1085
+ tags: ['Health'],
1086
+ summary: 'Liveness Probe',
1087
+ description: 'Kubernetes liveness probe - checks if the application is alive. If this fails, Kubernetes will restart the pod.',
1088
+ responses: {
1089
+ 200: {
1090
+ description: 'Application is alive',
1091
+ content: {
1092
+ 'application/json': {
1093
+ schema: {
1094
+ type: 'object',
1095
+ properties: {
1096
+ success: { type: 'boolean', example: true },
1097
+ data: {
1098
+ type: 'object',
1099
+ properties: {
1100
+ status: { type: 'string', example: 'alive' },
1101
+ timestamp: { type: 'string', format: 'date-time' }
1102
+ }
1103
+ }
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+ };
1112
+
1113
+ spec.paths['/health/ready'] = {
1114
+ get: {
1115
+ tags: ['Health'],
1116
+ summary: 'Readiness Probe',
1117
+ description: 'Kubernetes readiness probe - checks if the application is ready to receive traffic. If this fails, Kubernetes will remove the pod from service endpoints.',
1118
+ responses: {
1119
+ 200: {
1120
+ description: 'Application is ready to receive traffic',
1121
+ content: {
1122
+ 'application/json': {
1123
+ schema: {
1124
+ type: 'object',
1125
+ properties: {
1126
+ success: { type: 'boolean', example: true },
1127
+ data: {
1128
+ type: 'object',
1129
+ properties: {
1130
+ status: { type: 'string', example: 'ready' },
1131
+ database: {
1132
+ type: 'object',
1133
+ properties: {
1134
+ connected: { type: 'boolean', example: true },
1135
+ resources: { type: 'integer', example: 5 }
1136
+ }
1137
+ },
1138
+ timestamp: { type: 'string', format: 'date-time' }
1139
+ }
1140
+ }
1141
+ }
1142
+ }
1143
+ }
1144
+ }
1145
+ },
1146
+ 503: {
1147
+ description: 'Application is not ready',
1148
+ content: {
1149
+ 'application/json': {
1150
+ schema: {
1151
+ type: 'object',
1152
+ properties: {
1153
+ success: { type: 'boolean', example: false },
1154
+ error: {
1155
+ type: 'object',
1156
+ properties: {
1157
+ message: { type: 'string', example: 'Service not ready' },
1158
+ code: { type: 'string', example: 'NOT_READY' },
1159
+ details: {
1160
+ type: 'object',
1161
+ properties: {
1162
+ database: {
1163
+ type: 'object',
1164
+ properties: {
1165
+ connected: { type: 'boolean', example: false },
1166
+ resources: { type: 'integer', example: 0 }
1167
+ }
1168
+ }
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+ }
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+ }
1180
+ };
1181
+
1182
+ spec.tags.push({
1183
+ name: 'Health',
1184
+ description: 'Health check endpoints for monitoring and Kubernetes probes'
1185
+ });
1186
+
1187
+ // Add Prometheus metrics endpoint if MetricsPlugin is active
1188
+ const metricsPlugin = database.plugins?.metrics || database.plugins?.MetricsPlugin;
1189
+ if (metricsPlugin && metricsPlugin.config?.prometheus?.enabled) {
1190
+ const metricsPath = metricsPlugin.config.prometheus.path || '/metrics';
1191
+ const isIntegrated = metricsPlugin.config.prometheus.mode !== 'standalone';
1192
+
1193
+ // Only add to OpenAPI if using integrated mode (same server)
1194
+ if (isIntegrated) {
1195
+ spec.paths[metricsPath] = {
1196
+ get: {
1197
+ tags: ['Monitoring'],
1198
+ summary: 'Prometheus Metrics',
1199
+ description: 'Exposes application metrics in Prometheus text-based exposition format for monitoring and observability. ' +
1200
+ 'Metrics include operation counts, durations, errors, uptime, and resource statistics.',
1201
+ responses: {
1202
+ 200: {
1203
+ description: 'Metrics in Prometheus format',
1204
+ content: {
1205
+ 'text/plain': {
1206
+ schema: {
1207
+ type: 'string',
1208
+ example: '# HELP s3db_operations_total Total number of operations by type and resource\n' +
1209
+ '# TYPE s3db_operations_total counter\n' +
1210
+ 's3db_operations_total{operation="insert",resource="cars"} 1523\n' +
1211
+ 's3db_operations_total{operation="update",resource="cars"} 342\n\n' +
1212
+ '# HELP s3db_operation_duration_seconds Average operation duration in seconds\n' +
1213
+ '# TYPE s3db_operation_duration_seconds gauge\n' +
1214
+ 's3db_operation_duration_seconds{operation="insert",resource="cars"} 0.045\n\n' +
1215
+ '# HELP s3db_operation_errors_total Total number of operation errors\n' +
1216
+ '# TYPE s3db_operation_errors_total counter\n' +
1217
+ 's3db_operation_errors_total{operation="insert",resource="cars"} 12\n'
1218
+ }
1219
+ }
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+ };
1225
+
1226
+ spec.tags.push({
1227
+ name: 'Monitoring',
1228
+ description: 'Monitoring and observability endpoints (Prometheus)'
1229
+ });
1230
+ }
1231
+ }
1232
+
1233
+ return spec;
1234
+ }
1235
+
1236
+ export default {
1237
+ generateOpenAPISpec,
1238
+ generateResourceSchema,
1239
+ generateResourcePaths
1240
+ };