s3db.js 12.1.0 → 12.2.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.
- package/README.md +212 -196
- package/dist/s3db.cjs.js +1041 -1941
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1039 -1941
- package/dist/s3db.es.js.map +1 -1
- package/package.json +6 -1
- package/src/cli/index.js +954 -43
- package/src/cli/migration-manager.js +270 -0
- package/src/concerns/calculator.js +0 -4
- package/src/concerns/metadata-encoding.js +1 -21
- package/src/concerns/plugin-storage.js +17 -4
- package/src/concerns/typescript-generator.d.ts +171 -0
- package/src/concerns/typescript-generator.js +275 -0
- package/src/database.class.js +171 -28
- package/src/index.js +15 -9
- package/src/plugins/api/index.js +5 -2
- package/src/plugins/api/routes/resource-routes.js +86 -1
- package/src/plugins/api/server.js +79 -3
- package/src/plugins/api/utils/openapi-generator.js +195 -5
- package/src/plugins/backup/multi-backup-driver.class.js +0 -1
- package/src/plugins/backup.plugin.js +7 -14
- package/src/plugins/concerns/plugin-dependencies.js +73 -19
- package/src/plugins/eventual-consistency/analytics.js +0 -2
- package/src/plugins/eventual-consistency/consolidation.js +2 -13
- package/src/plugins/eventual-consistency/index.js +0 -1
- package/src/plugins/eventual-consistency/install.js +1 -1
- package/src/plugins/geo.plugin.js +5 -6
- package/src/plugins/importer/index.js +1 -1
- package/src/plugins/index.js +2 -1
- package/src/plugins/relation.plugin.js +11 -11
- package/src/plugins/replicator.plugin.js +12 -21
- package/src/plugins/s3-queue.plugin.js +4 -4
- package/src/plugins/scheduler.plugin.js +10 -12
- package/src/plugins/state-machine.plugin.js +8 -12
- package/src/plugins/tfstate/README.md +1 -1
- package/src/plugins/tfstate/errors.js +3 -3
- package/src/plugins/tfstate/index.js +41 -67
- package/src/plugins/ttl.plugin.js +3 -3
- package/src/resource.class.js +263 -61
- package/src/schema.class.js +0 -2
- package/src/testing/factory.class.js +286 -0
- package/src/testing/index.js +15 -0
- package/src/testing/seeder.class.js +183 -0
package/dist/s3db.es.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import crypto$1, { createHash } from 'crypto';
|
|
2
2
|
import { customAlphabet, urlAlphabet } from 'nanoid';
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
|
-
import { Hono } from 'hono';
|
|
5
|
-
import { serve } from '@hono/node-server';
|
|
6
|
-
import { swaggerUI } from '@hono/swagger-ui';
|
|
7
4
|
import { mkdir, copyFile, unlink, stat, access, readdir, writeFile, readFile, rm, watch } from 'fs/promises';
|
|
8
5
|
import fs, { createReadStream, createWriteStream, realpathSync as realpathSync$1, readlinkSync, readdirSync, readdir as readdir$2, lstatSync, existsSync } from 'fs';
|
|
9
6
|
import { pipeline } from 'stream/promises';
|
|
10
|
-
import path$1, { join } from 'path';
|
|
7
|
+
import path$1, { join, dirname } from 'path';
|
|
11
8
|
import { Transform, Writable } from 'stream';
|
|
12
9
|
import zlib from 'node:zlib';
|
|
13
10
|
import os from 'os';
|
|
@@ -209,8 +206,6 @@ function calculateUTF8Bytes(str) {
|
|
|
209
206
|
function clearUTF8Memory() {
|
|
210
207
|
utf8BytesMemory.clear();
|
|
211
208
|
}
|
|
212
|
-
const clearUTF8Memo = clearUTF8Memory;
|
|
213
|
-
const clearUTF8Cache = clearUTF8Memory;
|
|
214
209
|
function calculateAttributeNamesSize(mappedObject) {
|
|
215
210
|
let totalSize = 0;
|
|
216
211
|
for (const key of Object.keys(mappedObject)) {
|
|
@@ -1604,18 +1599,6 @@ function metadataDecode(value) {
|
|
|
1604
1599
|
}
|
|
1605
1600
|
}
|
|
1606
1601
|
}
|
|
1607
|
-
const len = value.length;
|
|
1608
|
-
if (len > 0 && len % 4 === 0) {
|
|
1609
|
-
if (/^[A-Za-z0-9+/]+=*$/.test(value)) {
|
|
1610
|
-
try {
|
|
1611
|
-
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
1612
|
-
if (/[^\x00-\x7F]/.test(decoded) && Buffer.from(decoded, "utf8").toString("base64") === value) {
|
|
1613
|
-
return decoded;
|
|
1614
|
-
}
|
|
1615
|
-
} catch {
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
1602
|
return value;
|
|
1620
1603
|
}
|
|
1621
1604
|
|
|
@@ -1702,11 +1685,22 @@ class PluginStorage {
|
|
|
1702
1685
|
}
|
|
1703
1686
|
}
|
|
1704
1687
|
/**
|
|
1705
|
-
*
|
|
1706
|
-
*
|
|
1688
|
+
* Batch set multiple items
|
|
1689
|
+
*
|
|
1690
|
+
* @param {Array<{key: string, data: Object, options?: Object}>} items - Items to save
|
|
1691
|
+
* @returns {Promise<Array<{ok: boolean, key: string, error?: Error}>>} Results
|
|
1707
1692
|
*/
|
|
1708
|
-
async
|
|
1709
|
-
|
|
1693
|
+
async batchSet(items) {
|
|
1694
|
+
const results = [];
|
|
1695
|
+
for (const item of items) {
|
|
1696
|
+
try {
|
|
1697
|
+
await this.set(item.key, item.data, item.options || {});
|
|
1698
|
+
results.push({ ok: true, key: item.key });
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
results.push({ ok: false, key: item.key, error });
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return results;
|
|
1710
1704
|
}
|
|
1711
1705
|
/**
|
|
1712
1706
|
* Get data with automatic metadata decoding and TTL check
|
|
@@ -2454,1800 +2448,100 @@ const PluginObject = {
|
|
|
2454
2448
|
}
|
|
2455
2449
|
};
|
|
2456
2450
|
|
|
2457
|
-
function success(data, options = {}) {
|
|
2458
|
-
const { status = 200, meta = {} } = options;
|
|
2459
|
-
return {
|
|
2460
|
-
success: true,
|
|
2461
|
-
data,
|
|
2462
|
-
meta: {
|
|
2463
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2464
|
-
...meta
|
|
2465
|
-
},
|
|
2466
|
-
_status: status
|
|
2467
|
-
};
|
|
2468
|
-
}
|
|
2469
|
-
function error(error2, options = {}) {
|
|
2470
|
-
const { status = 500, code = "INTERNAL_ERROR", details = {} } = options;
|
|
2471
|
-
const errorMessage = error2 instanceof Error ? error2.message : error2;
|
|
2472
|
-
const errorStack = error2 instanceof Error && process.env.NODE_ENV !== "production" ? error2.stack : void 0;
|
|
2473
|
-
return {
|
|
2474
|
-
success: false,
|
|
2475
|
-
error: {
|
|
2476
|
-
message: errorMessage,
|
|
2477
|
-
code,
|
|
2478
|
-
details,
|
|
2479
|
-
stack: errorStack
|
|
2480
|
-
},
|
|
2481
|
-
meta: {
|
|
2482
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2483
|
-
},
|
|
2484
|
-
_status: status
|
|
2485
|
-
};
|
|
2486
|
-
}
|
|
2487
|
-
function list(items, pagination = {}) {
|
|
2488
|
-
const { total, page, pageSize, pageCount } = pagination;
|
|
2489
|
-
return {
|
|
2490
|
-
success: true,
|
|
2491
|
-
data: items,
|
|
2492
|
-
pagination: {
|
|
2493
|
-
total: total || items.length,
|
|
2494
|
-
page: page || 1,
|
|
2495
|
-
pageSize: pageSize || items.length,
|
|
2496
|
-
pageCount: pageCount || 1
|
|
2497
|
-
},
|
|
2498
|
-
meta: {
|
|
2499
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2500
|
-
},
|
|
2501
|
-
_status: 200
|
|
2502
|
-
};
|
|
2503
|
-
}
|
|
2504
|
-
function created(data, location) {
|
|
2505
|
-
return {
|
|
2506
|
-
success: true,
|
|
2507
|
-
data,
|
|
2508
|
-
meta: {
|
|
2509
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2510
|
-
location
|
|
2511
|
-
},
|
|
2512
|
-
_status: 201
|
|
2513
|
-
};
|
|
2514
|
-
}
|
|
2515
|
-
function noContent() {
|
|
2516
|
-
return {
|
|
2517
|
-
success: true,
|
|
2518
|
-
data: null,
|
|
2519
|
-
meta: {
|
|
2520
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2521
|
-
},
|
|
2522
|
-
_status: 204
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
function notFound(resource, id) {
|
|
2526
|
-
return error(`${resource} with id '${id}' not found`, {
|
|
2527
|
-
status: 404,
|
|
2528
|
-
code: "NOT_FOUND",
|
|
2529
|
-
details: { resource, id }
|
|
2530
|
-
});
|
|
2531
|
-
}
|
|
2532
|
-
function payloadTooLarge(size, limit) {
|
|
2533
|
-
return error("Request payload too large", {
|
|
2534
|
-
status: 413,
|
|
2535
|
-
code: "PAYLOAD_TOO_LARGE",
|
|
2536
|
-
details: {
|
|
2537
|
-
receivedSize: size,
|
|
2538
|
-
maxSize: limit,
|
|
2539
|
-
receivedMB: (size / 1024 / 1024).toFixed(2),
|
|
2540
|
-
maxMB: (limit / 1024 / 1024).toFixed(2)
|
|
2541
|
-
}
|
|
2542
|
-
});
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
const errorStatusMap = {
|
|
2546
|
-
"ValidationError": 400,
|
|
2547
|
-
"InvalidResourceItem": 400,
|
|
2548
|
-
"ResourceNotFound": 404,
|
|
2549
|
-
"NoSuchKey": 404,
|
|
2550
|
-
"NoSuchBucket": 404,
|
|
2551
|
-
"PartitionError": 400,
|
|
2552
|
-
"CryptoError": 500,
|
|
2553
|
-
"SchemaError": 400,
|
|
2554
|
-
"QueueError": 500,
|
|
2555
|
-
"ResourceError": 500
|
|
2556
|
-
};
|
|
2557
|
-
function getStatusFromError(err) {
|
|
2558
|
-
if (err.name && errorStatusMap[err.name]) {
|
|
2559
|
-
return errorStatusMap[err.name];
|
|
2560
|
-
}
|
|
2561
|
-
if (err.constructor && err.constructor.name && errorStatusMap[err.constructor.name]) {
|
|
2562
|
-
return errorStatusMap[err.constructor.name];
|
|
2563
|
-
}
|
|
2564
|
-
if (err.message) {
|
|
2565
|
-
if (err.message.includes("not found") || err.message.includes("does not exist")) {
|
|
2566
|
-
return 404;
|
|
2567
|
-
}
|
|
2568
|
-
if (err.message.includes("validation") || err.message.includes("invalid")) {
|
|
2569
|
-
return 400;
|
|
2570
|
-
}
|
|
2571
|
-
if (err.message.includes("unauthorized") || err.message.includes("authentication")) {
|
|
2572
|
-
return 401;
|
|
2573
|
-
}
|
|
2574
|
-
if (err.message.includes("forbidden") || err.message.includes("permission")) {
|
|
2575
|
-
return 403;
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
return 500;
|
|
2579
|
-
}
|
|
2580
|
-
function errorHandler(err, c) {
|
|
2581
|
-
const status = getStatusFromError(err);
|
|
2582
|
-
const code = err.name || "INTERNAL_ERROR";
|
|
2583
|
-
const details = {};
|
|
2584
|
-
if (err.resource) details.resource = err.resource;
|
|
2585
|
-
if (err.bucket) details.bucket = err.bucket;
|
|
2586
|
-
if (err.key) details.key = err.key;
|
|
2587
|
-
if (err.operation) details.operation = err.operation;
|
|
2588
|
-
if (err.suggestion) details.suggestion = err.suggestion;
|
|
2589
|
-
if (err.availableResources) details.availableResources = err.availableResources;
|
|
2590
|
-
const response = error(err, {
|
|
2591
|
-
status,
|
|
2592
|
-
code,
|
|
2593
|
-
details
|
|
2594
|
-
});
|
|
2595
|
-
if (status >= 500) {
|
|
2596
|
-
console.error("[API Plugin] Error:", {
|
|
2597
|
-
message: err.message,
|
|
2598
|
-
code,
|
|
2599
|
-
status,
|
|
2600
|
-
stack: err.stack,
|
|
2601
|
-
details
|
|
2602
|
-
});
|
|
2603
|
-
} else if (status >= 400 && status < 500 && c.get("verbose")) {
|
|
2604
|
-
console.warn("[API Plugin] Client error:", {
|
|
2605
|
-
message: err.message,
|
|
2606
|
-
code,
|
|
2607
|
-
status,
|
|
2608
|
-
details
|
|
2609
|
-
});
|
|
2610
|
-
}
|
|
2611
|
-
return c.json(response, response._status);
|
|
2612
|
-
}
|
|
2613
|
-
function asyncHandler(fn) {
|
|
2614
|
-
return async (c) => {
|
|
2615
|
-
try {
|
|
2616
|
-
return await fn(c);
|
|
2617
|
-
} catch (err) {
|
|
2618
|
-
return errorHandler(err, c);
|
|
2619
|
-
}
|
|
2620
|
-
};
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
function createResourceRoutes(resource, version, config = {}) {
|
|
2624
|
-
const app = new Hono();
|
|
2625
|
-
const {
|
|
2626
|
-
methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
2627
|
-
customMiddleware = [],
|
|
2628
|
-
enableValidation = true
|
|
2629
|
-
} = config;
|
|
2630
|
-
const resourceName = resource.name;
|
|
2631
|
-
const basePath = `/${version}/${resourceName}`;
|
|
2632
|
-
customMiddleware.forEach((middleware) => {
|
|
2633
|
-
app.use("*", middleware);
|
|
2634
|
-
});
|
|
2635
|
-
if (methods.includes("GET")) {
|
|
2636
|
-
app.get("/", asyncHandler(async (c) => {
|
|
2637
|
-
const query = c.req.query();
|
|
2638
|
-
const limit = parseInt(query.limit) || 100;
|
|
2639
|
-
const offset = parseInt(query.offset) || 0;
|
|
2640
|
-
const partition = query.partition;
|
|
2641
|
-
const partitionValues = query.partitionValues ? JSON.parse(query.partitionValues) : void 0;
|
|
2642
|
-
const reservedKeys = ["limit", "offset", "partition", "partitionValues", "sort"];
|
|
2643
|
-
const filters = {};
|
|
2644
|
-
for (const [key, value] of Object.entries(query)) {
|
|
2645
|
-
if (!reservedKeys.includes(key)) {
|
|
2646
|
-
try {
|
|
2647
|
-
filters[key] = JSON.parse(value);
|
|
2648
|
-
} catch {
|
|
2649
|
-
filters[key] = value;
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
let items;
|
|
2654
|
-
let total;
|
|
2655
|
-
if (Object.keys(filters).length > 0) {
|
|
2656
|
-
items = await resource.query(filters, { limit: limit + offset });
|
|
2657
|
-
items = items.slice(offset, offset + limit);
|
|
2658
|
-
total = items.length;
|
|
2659
|
-
} else if (partition && partitionValues) {
|
|
2660
|
-
items = await resource.listPartition({
|
|
2661
|
-
partition,
|
|
2662
|
-
partitionValues,
|
|
2663
|
-
limit: limit + offset
|
|
2664
|
-
});
|
|
2665
|
-
items = items.slice(offset, offset + limit);
|
|
2666
|
-
total = items.length;
|
|
2667
|
-
} else {
|
|
2668
|
-
items = await resource.list({ limit: limit + offset });
|
|
2669
|
-
items = items.slice(offset, offset + limit);
|
|
2670
|
-
total = items.length;
|
|
2671
|
-
}
|
|
2672
|
-
const response = list(items, {
|
|
2673
|
-
total,
|
|
2674
|
-
page: Math.floor(offset / limit) + 1,
|
|
2675
|
-
pageSize: limit,
|
|
2676
|
-
pageCount: Math.ceil(total / limit)
|
|
2677
|
-
});
|
|
2678
|
-
c.header("X-Total-Count", total.toString());
|
|
2679
|
-
c.header("X-Page-Count", Math.ceil(total / limit).toString());
|
|
2680
|
-
return c.json(response, response._status);
|
|
2681
|
-
}));
|
|
2682
|
-
}
|
|
2683
|
-
if (methods.includes("GET")) {
|
|
2684
|
-
app.get("/:id", asyncHandler(async (c) => {
|
|
2685
|
-
const id = c.req.param("id");
|
|
2686
|
-
const query = c.req.query();
|
|
2687
|
-
const partition = query.partition;
|
|
2688
|
-
const partitionValues = query.partitionValues ? JSON.parse(query.partitionValues) : void 0;
|
|
2689
|
-
let item;
|
|
2690
|
-
if (partition && partitionValues) {
|
|
2691
|
-
item = await resource.getFromPartition({
|
|
2692
|
-
id,
|
|
2693
|
-
partitionName: partition,
|
|
2694
|
-
partitionValues
|
|
2695
|
-
});
|
|
2696
|
-
} else {
|
|
2697
|
-
item = await resource.get(id);
|
|
2698
|
-
}
|
|
2699
|
-
if (!item) {
|
|
2700
|
-
const response2 = notFound(resourceName, id);
|
|
2701
|
-
return c.json(response2, response2._status);
|
|
2702
|
-
}
|
|
2703
|
-
const response = success(item);
|
|
2704
|
-
return c.json(response, response._status);
|
|
2705
|
-
}));
|
|
2706
|
-
}
|
|
2707
|
-
if (methods.includes("POST")) {
|
|
2708
|
-
app.post("/", asyncHandler(async (c) => {
|
|
2709
|
-
const data = await c.req.json();
|
|
2710
|
-
const item = await resource.insert(data);
|
|
2711
|
-
const location = `${basePath}/${item.id}`;
|
|
2712
|
-
const response = created(item, location);
|
|
2713
|
-
c.header("Location", location);
|
|
2714
|
-
return c.json(response, response._status);
|
|
2715
|
-
}));
|
|
2716
|
-
}
|
|
2717
|
-
if (methods.includes("PUT")) {
|
|
2718
|
-
app.put("/:id", asyncHandler(async (c) => {
|
|
2719
|
-
const id = c.req.param("id");
|
|
2720
|
-
const data = await c.req.json();
|
|
2721
|
-
const existing = await resource.get(id);
|
|
2722
|
-
if (!existing) {
|
|
2723
|
-
const response2 = notFound(resourceName, id);
|
|
2724
|
-
return c.json(response2, response2._status);
|
|
2725
|
-
}
|
|
2726
|
-
const updated = await resource.update(id, data);
|
|
2727
|
-
const response = success(updated);
|
|
2728
|
-
return c.json(response, response._status);
|
|
2729
|
-
}));
|
|
2730
|
-
}
|
|
2731
|
-
if (methods.includes("PATCH")) {
|
|
2732
|
-
app.patch("/:id", asyncHandler(async (c) => {
|
|
2733
|
-
const id = c.req.param("id");
|
|
2734
|
-
const data = await c.req.json();
|
|
2735
|
-
const existing = await resource.get(id);
|
|
2736
|
-
if (!existing) {
|
|
2737
|
-
const response2 = notFound(resourceName, id);
|
|
2738
|
-
return c.json(response2, response2._status);
|
|
2739
|
-
}
|
|
2740
|
-
const merged = { ...existing, ...data, id };
|
|
2741
|
-
const updated = await resource.update(id, merged);
|
|
2742
|
-
const response = success(updated);
|
|
2743
|
-
return c.json(response, response._status);
|
|
2744
|
-
}));
|
|
2745
|
-
}
|
|
2746
|
-
if (methods.includes("DELETE")) {
|
|
2747
|
-
app.delete("/:id", asyncHandler(async (c) => {
|
|
2748
|
-
const id = c.req.param("id");
|
|
2749
|
-
const existing = await resource.get(id);
|
|
2750
|
-
if (!existing) {
|
|
2751
|
-
const response2 = notFound(resourceName, id);
|
|
2752
|
-
return c.json(response2, response2._status);
|
|
2753
|
-
}
|
|
2754
|
-
await resource.delete(id);
|
|
2755
|
-
const response = noContent();
|
|
2756
|
-
return c.json(response, response._status);
|
|
2757
|
-
}));
|
|
2758
|
-
}
|
|
2759
|
-
if (methods.includes("HEAD")) {
|
|
2760
|
-
app.on("HEAD", "/", asyncHandler(async (c) => {
|
|
2761
|
-
const total = await resource.count();
|
|
2762
|
-
const allItems = await resource.list({ limit: 1e3 });
|
|
2763
|
-
const stats = {
|
|
2764
|
-
total,
|
|
2765
|
-
version: resource.config?.currentVersion || resource.version || "v1"
|
|
2766
|
-
};
|
|
2767
|
-
c.header("X-Total-Count", total.toString());
|
|
2768
|
-
c.header("X-Resource-Version", stats.version);
|
|
2769
|
-
c.header("X-Schema-Fields", Object.keys(resource.config?.attributes || {}).length.toString());
|
|
2770
|
-
return c.body(null, 200);
|
|
2771
|
-
}));
|
|
2772
|
-
app.on("HEAD", "/:id", asyncHandler(async (c) => {
|
|
2773
|
-
const id = c.req.param("id");
|
|
2774
|
-
const item = await resource.get(id);
|
|
2775
|
-
if (!item) {
|
|
2776
|
-
return c.body(null, 404);
|
|
2777
|
-
}
|
|
2778
|
-
if (item.updatedAt) {
|
|
2779
|
-
c.header("Last-Modified", new Date(item.updatedAt).toUTCString());
|
|
2780
|
-
}
|
|
2781
|
-
return c.body(null, 200);
|
|
2782
|
-
}));
|
|
2783
|
-
}
|
|
2784
|
-
if (methods.includes("OPTIONS")) {
|
|
2785
|
-
app.options("/", asyncHandler(async (c) => {
|
|
2786
|
-
c.header("Allow", methods.join(", "));
|
|
2787
|
-
const total = await resource.count();
|
|
2788
|
-
const schema = resource.config?.attributes || {};
|
|
2789
|
-
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
2790
|
-
const metadata = {
|
|
2791
|
-
resource: resourceName,
|
|
2792
|
-
version: version2,
|
|
2793
|
-
totalRecords: total,
|
|
2794
|
-
allowedMethods: methods,
|
|
2795
|
-
schema: Object.entries(schema).map(([name, def]) => ({
|
|
2796
|
-
name,
|
|
2797
|
-
type: typeof def === "string" ? def.split("|")[0] : def.type,
|
|
2798
|
-
rules: typeof def === "string" ? def.split("|").slice(1) : []
|
|
2799
|
-
})),
|
|
2800
|
-
endpoints: {
|
|
2801
|
-
list: `/${version2}/${resourceName}`,
|
|
2802
|
-
get: `/${version2}/${resourceName}/:id`,
|
|
2803
|
-
create: `/${version2}/${resourceName}`,
|
|
2804
|
-
update: `/${version2}/${resourceName}/:id`,
|
|
2805
|
-
delete: `/${version2}/${resourceName}/:id`
|
|
2806
|
-
},
|
|
2807
|
-
queryParameters: {
|
|
2808
|
-
limit: "number (1-1000, default: 100)",
|
|
2809
|
-
offset: "number (min: 0, default: 0)",
|
|
2810
|
-
partition: "string (partition name)",
|
|
2811
|
-
partitionValues: "JSON string",
|
|
2812
|
-
"[any field]": "any (filter by field value)"
|
|
2813
|
-
}
|
|
2814
|
-
};
|
|
2815
|
-
return c.json(metadata);
|
|
2816
|
-
}));
|
|
2817
|
-
app.options("/:id", (c) => {
|
|
2818
|
-
c.header("Allow", methods.filter((m) => m !== "POST").join(", "));
|
|
2819
|
-
return c.body(null, 204);
|
|
2820
|
-
});
|
|
2821
|
-
}
|
|
2822
|
-
return app;
|
|
2823
|
-
}
|
|
2824
|
-
|
|
2825
|
-
function mapFieldTypeToOpenAPI(fieldType) {
|
|
2826
|
-
const type = fieldType.split("|")[0].trim();
|
|
2827
|
-
const typeMap = {
|
|
2828
|
-
"string": { type: "string" },
|
|
2829
|
-
"number": { type: "number" },
|
|
2830
|
-
"integer": { type: "integer" },
|
|
2831
|
-
"boolean": { type: "boolean" },
|
|
2832
|
-
"array": { type: "array", items: { type: "string" } },
|
|
2833
|
-
"object": { type: "object" },
|
|
2834
|
-
"json": { type: "object" },
|
|
2835
|
-
"secret": { type: "string", format: "password" },
|
|
2836
|
-
"email": { type: "string", format: "email" },
|
|
2837
|
-
"url": { type: "string", format: "uri" },
|
|
2838
|
-
"date": { type: "string", format: "date" },
|
|
2839
|
-
"datetime": { type: "string", format: "date-time" },
|
|
2840
|
-
"ip4": { type: "string", format: "ipv4", description: "IPv4 address" },
|
|
2841
|
-
"ip6": { type: "string", format: "ipv6", description: "IPv6 address" },
|
|
2842
|
-
"embedding": { type: "array", items: { type: "number" }, description: "Vector embedding" }
|
|
2843
|
-
};
|
|
2844
|
-
if (type.startsWith("embedding:")) {
|
|
2845
|
-
const length = parseInt(type.split(":")[1]);
|
|
2846
|
-
return {
|
|
2847
|
-
type: "array",
|
|
2848
|
-
items: { type: "number" },
|
|
2849
|
-
minItems: length,
|
|
2850
|
-
maxItems: length,
|
|
2851
|
-
description: `Vector embedding (${length} dimensions)`
|
|
2852
|
-
};
|
|
2853
|
-
}
|
|
2854
|
-
return typeMap[type] || { type: "string" };
|
|
2855
|
-
}
|
|
2856
|
-
function extractValidationRules(fieldDef) {
|
|
2857
|
-
const rules = {};
|
|
2858
|
-
const parts = fieldDef.split("|");
|
|
2859
|
-
for (const part of parts) {
|
|
2860
|
-
const [rule, value] = part.split(":").map((s) => s.trim());
|
|
2861
|
-
switch (rule) {
|
|
2862
|
-
case "required":
|
|
2863
|
-
rules.required = true;
|
|
2864
|
-
break;
|
|
2865
|
-
case "min":
|
|
2866
|
-
rules.minimum = parseFloat(value);
|
|
2867
|
-
break;
|
|
2868
|
-
case "max":
|
|
2869
|
-
rules.maximum = parseFloat(value);
|
|
2870
|
-
break;
|
|
2871
|
-
case "minlength":
|
|
2872
|
-
rules.minLength = parseInt(value);
|
|
2873
|
-
break;
|
|
2874
|
-
case "maxlength":
|
|
2875
|
-
rules.maxLength = parseInt(value);
|
|
2876
|
-
break;
|
|
2877
|
-
case "pattern":
|
|
2878
|
-
rules.pattern = value;
|
|
2879
|
-
break;
|
|
2880
|
-
case "enum":
|
|
2881
|
-
rules.enum = value.split(",").map((v) => v.trim());
|
|
2882
|
-
break;
|
|
2883
|
-
case "default":
|
|
2884
|
-
rules.default = value;
|
|
2885
|
-
break;
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
return rules;
|
|
2889
|
-
}
|
|
2890
|
-
function generateResourceSchema(resource) {
|
|
2891
|
-
const properties = {};
|
|
2892
|
-
const required = [];
|
|
2893
|
-
const attributes = resource.config?.attributes || resource.attributes || {};
|
|
2894
|
-
properties.id = {
|
|
2895
|
-
type: "string",
|
|
2896
|
-
description: "Unique identifier for the resource",
|
|
2897
|
-
example: "2_gDTpeU6EI0e8B92n_R3Y",
|
|
2898
|
-
readOnly: true
|
|
2899
|
-
};
|
|
2900
|
-
for (const [fieldName, fieldDef] of Object.entries(attributes)) {
|
|
2901
|
-
if (typeof fieldDef === "object" && fieldDef.type) {
|
|
2902
|
-
const baseType = mapFieldTypeToOpenAPI(fieldDef.type);
|
|
2903
|
-
properties[fieldName] = {
|
|
2904
|
-
...baseType,
|
|
2905
|
-
description: fieldDef.description || void 0
|
|
2906
|
-
};
|
|
2907
|
-
if (fieldDef.required) {
|
|
2908
|
-
required.push(fieldName);
|
|
2909
|
-
}
|
|
2910
|
-
if (fieldDef.type === "object" && fieldDef.props) {
|
|
2911
|
-
properties[fieldName].properties = {};
|
|
2912
|
-
for (const [propName, propDef] of Object.entries(fieldDef.props)) {
|
|
2913
|
-
const propType = typeof propDef === "string" ? propDef : propDef.type;
|
|
2914
|
-
properties[fieldName].properties[propName] = mapFieldTypeToOpenAPI(propType);
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
if (fieldDef.type === "array" && fieldDef.items) {
|
|
2918
|
-
properties[fieldName].items = mapFieldTypeToOpenAPI(fieldDef.items);
|
|
2919
|
-
}
|
|
2920
|
-
} else if (typeof fieldDef === "string") {
|
|
2921
|
-
const baseType = mapFieldTypeToOpenAPI(fieldDef);
|
|
2922
|
-
const rules = extractValidationRules(fieldDef);
|
|
2923
|
-
properties[fieldName] = {
|
|
2924
|
-
...baseType,
|
|
2925
|
-
...rules
|
|
2926
|
-
};
|
|
2927
|
-
if (rules.required) {
|
|
2928
|
-
required.push(fieldName);
|
|
2929
|
-
delete properties[fieldName].required;
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
2932
|
-
}
|
|
2933
|
-
return {
|
|
2934
|
-
type: "object",
|
|
2935
|
-
properties,
|
|
2936
|
-
required: required.length > 0 ? required : void 0
|
|
2937
|
-
};
|
|
2938
|
-
}
|
|
2939
|
-
function generateResourcePaths(resource, version, config = {}) {
|
|
2940
|
-
const resourceName = resource.name;
|
|
2941
|
-
const basePath = `/${version}/${resourceName}`;
|
|
2942
|
-
const schema = generateResourceSchema(resource);
|
|
2943
|
-
const methods = config.methods || ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
2944
|
-
const authMethods = config.auth || [];
|
|
2945
|
-
const requiresAuth = authMethods && authMethods.length > 0;
|
|
2946
|
-
const paths = {};
|
|
2947
|
-
const security = [];
|
|
2948
|
-
if (requiresAuth) {
|
|
2949
|
-
if (authMethods.includes("jwt")) security.push({ bearerAuth: [] });
|
|
2950
|
-
if (authMethods.includes("apiKey")) security.push({ apiKeyAuth: [] });
|
|
2951
|
-
if (authMethods.includes("basic")) security.push({ basicAuth: [] });
|
|
2952
|
-
}
|
|
2953
|
-
const partitions = resource.config?.options?.partitions || resource.config?.partitions || resource.partitions || {};
|
|
2954
|
-
const partitionNames = Object.keys(partitions);
|
|
2955
|
-
const hasPartitions = partitionNames.length > 0;
|
|
2956
|
-
let partitionDescription = "Partition name for filtering";
|
|
2957
|
-
let partitionValuesDescription = "Partition values as JSON string";
|
|
2958
|
-
let partitionExample = void 0;
|
|
2959
|
-
let partitionValuesExample = void 0;
|
|
2960
|
-
if (hasPartitions) {
|
|
2961
|
-
const partitionDocs = partitionNames.map((name) => {
|
|
2962
|
-
const partition = partitions[name];
|
|
2963
|
-
const fields = Object.keys(partition.fields || {});
|
|
2964
|
-
const fieldTypes = Object.entries(partition.fields || {}).map(([field, type]) => `${field}: ${type}`).join(", ");
|
|
2965
|
-
return `- **${name}**: Filters by ${fields.join(", ")} (${fieldTypes})`;
|
|
2966
|
-
}).join("\n");
|
|
2967
|
-
partitionDescription = `Available partitions:
|
|
2968
|
-
${partitionDocs}`;
|
|
2969
|
-
const examplePartition = partitionNames[0];
|
|
2970
|
-
const exampleFields = partitions[examplePartition]?.fields || {};
|
|
2971
|
-
Object.entries(exampleFields).map(([field, type]) => `"${field}": <${type} value>`).join(", ");
|
|
2972
|
-
partitionValuesDescription = `Partition field values as JSON string. Must match the structure of the selected partition.
|
|
2973
|
-
|
|
2974
|
-
Example for "${examplePartition}" partition: \`{"${Object.keys(exampleFields)[0]}": "value"}\``;
|
|
2975
|
-
partitionExample = examplePartition;
|
|
2976
|
-
const firstField = Object.keys(exampleFields)[0];
|
|
2977
|
-
const firstFieldType = exampleFields[firstField];
|
|
2978
|
-
let exampleValue = "example";
|
|
2979
|
-
if (firstFieldType === "number" || firstFieldType === "integer") {
|
|
2980
|
-
exampleValue = 123;
|
|
2981
|
-
} else if (firstFieldType === "boolean") {
|
|
2982
|
-
exampleValue = true;
|
|
2983
|
-
}
|
|
2984
|
-
partitionValuesExample = JSON.stringify({ [firstField]: exampleValue });
|
|
2985
|
-
}
|
|
2986
|
-
const attributeQueryParams = [];
|
|
2987
|
-
if (hasPartitions) {
|
|
2988
|
-
const partitionFieldsSet = /* @__PURE__ */ new Set();
|
|
2989
|
-
for (const [partitionName, partition] of Object.entries(partitions)) {
|
|
2990
|
-
const fields = partition.fields || {};
|
|
2991
|
-
for (const fieldName of Object.keys(fields)) {
|
|
2992
|
-
partitionFieldsSet.add(fieldName);
|
|
2993
|
-
}
|
|
2994
|
-
}
|
|
2995
|
-
const attributes = resource.config?.attributes || resource.attributes || {};
|
|
2996
|
-
for (const fieldName of partitionFieldsSet) {
|
|
2997
|
-
const fieldDef = attributes[fieldName];
|
|
2998
|
-
if (!fieldDef) continue;
|
|
2999
|
-
let fieldType;
|
|
3000
|
-
if (typeof fieldDef === "object" && fieldDef.type) {
|
|
3001
|
-
fieldType = fieldDef.type;
|
|
3002
|
-
} else if (typeof fieldDef === "string") {
|
|
3003
|
-
fieldType = fieldDef.split("|")[0].trim();
|
|
3004
|
-
} else {
|
|
3005
|
-
fieldType = "string";
|
|
3006
|
-
}
|
|
3007
|
-
const openAPIType = mapFieldTypeToOpenAPI(fieldType);
|
|
3008
|
-
attributeQueryParams.push({
|
|
3009
|
-
name: fieldName,
|
|
3010
|
-
in: "query",
|
|
3011
|
-
description: `Filter by ${fieldName} field (indexed via partitions for efficient querying). Value will be parsed as JSON if possible, otherwise treated as string.`,
|
|
3012
|
-
required: false,
|
|
3013
|
-
schema: openAPIType
|
|
3014
|
-
});
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
if (methods.includes("GET")) {
|
|
3018
|
-
paths[basePath] = {
|
|
3019
|
-
get: {
|
|
3020
|
-
tags: [resourceName],
|
|
3021
|
-
summary: `List ${resourceName}`,
|
|
3022
|
-
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.
|
|
3023
|
-
|
|
3024
|
-
**Pagination**: Use \`limit\` and \`offset\` to paginate results. For example:
|
|
3025
|
-
- First page (10 items): \`?limit=10&offset=0\`
|
|
3026
|
-
- Second page: \`?limit=10&offset=10\`
|
|
3027
|
-
- Third page: \`?limit=10&offset=20\`
|
|
3028
|
-
|
|
3029
|
-
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." : ""}`,
|
|
3030
|
-
parameters: [
|
|
3031
|
-
{
|
|
3032
|
-
name: "limit",
|
|
3033
|
-
in: "query",
|
|
3034
|
-
description: "Maximum number of items to return per page (page size)",
|
|
3035
|
-
schema: { type: "integer", default: 100, minimum: 1, maximum: 1e3 },
|
|
3036
|
-
example: 10
|
|
3037
|
-
},
|
|
3038
|
-
{
|
|
3039
|
-
name: "offset",
|
|
3040
|
-
in: "query",
|
|
3041
|
-
description: "Number of items to skip before starting to return results. Use for pagination: offset = (page - 1) * limit",
|
|
3042
|
-
schema: { type: "integer", default: 0, minimum: 0 },
|
|
3043
|
-
example: 0
|
|
3044
|
-
},
|
|
3045
|
-
...hasPartitions ? [
|
|
3046
|
-
{
|
|
3047
|
-
name: "partition",
|
|
3048
|
-
in: "query",
|
|
3049
|
-
description: partitionDescription,
|
|
3050
|
-
schema: {
|
|
3051
|
-
type: "string",
|
|
3052
|
-
enum: partitionNames
|
|
3053
|
-
},
|
|
3054
|
-
example: partitionExample
|
|
3055
|
-
},
|
|
3056
|
-
{
|
|
3057
|
-
name: "partitionValues",
|
|
3058
|
-
in: "query",
|
|
3059
|
-
description: partitionValuesDescription,
|
|
3060
|
-
schema: { type: "string" },
|
|
3061
|
-
example: partitionValuesExample
|
|
3062
|
-
}
|
|
3063
|
-
] : [],
|
|
3064
|
-
...attributeQueryParams
|
|
3065
|
-
],
|
|
3066
|
-
responses: {
|
|
3067
|
-
200: {
|
|
3068
|
-
description: "Successful response",
|
|
3069
|
-
content: {
|
|
3070
|
-
"application/json": {
|
|
3071
|
-
schema: {
|
|
3072
|
-
type: "object",
|
|
3073
|
-
properties: {
|
|
3074
|
-
success: { type: "boolean", example: true },
|
|
3075
|
-
data: {
|
|
3076
|
-
type: "array",
|
|
3077
|
-
items: schema
|
|
3078
|
-
},
|
|
3079
|
-
pagination: {
|
|
3080
|
-
type: "object",
|
|
3081
|
-
description: "Pagination metadata for the current request",
|
|
3082
|
-
properties: {
|
|
3083
|
-
total: {
|
|
3084
|
-
type: "integer",
|
|
3085
|
-
description: "Total number of items available",
|
|
3086
|
-
example: 150
|
|
3087
|
-
},
|
|
3088
|
-
page: {
|
|
3089
|
-
type: "integer",
|
|
3090
|
-
description: "Current page number (1-indexed)",
|
|
3091
|
-
example: 1
|
|
3092
|
-
},
|
|
3093
|
-
pageSize: {
|
|
3094
|
-
type: "integer",
|
|
3095
|
-
description: "Number of items per page (same as limit parameter)",
|
|
3096
|
-
example: 10
|
|
3097
|
-
},
|
|
3098
|
-
pageCount: {
|
|
3099
|
-
type: "integer",
|
|
3100
|
-
description: "Total number of pages available",
|
|
3101
|
-
example: 15
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
},
|
|
3109
|
-
headers: {
|
|
3110
|
-
"X-Total-Count": {
|
|
3111
|
-
description: "Total number of records",
|
|
3112
|
-
schema: { type: "integer" }
|
|
3113
|
-
},
|
|
3114
|
-
"X-Page-Count": {
|
|
3115
|
-
description: "Total number of pages",
|
|
3116
|
-
schema: { type: "integer" }
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
},
|
|
3121
|
-
security: security.length > 0 ? security : void 0
|
|
3122
|
-
}
|
|
3123
|
-
};
|
|
3124
|
-
}
|
|
3125
|
-
if (methods.includes("GET")) {
|
|
3126
|
-
paths[`${basePath}/{id}`] = {
|
|
3127
|
-
get: {
|
|
3128
|
-
tags: [resourceName],
|
|
3129
|
-
summary: `Get ${resourceName} by ID`,
|
|
3130
|
-
description: `Retrieve a single ${resourceName} by its ID${hasPartitions ? ". Optionally specify a partition for more efficient retrieval." : ""}`,
|
|
3131
|
-
parameters: [
|
|
3132
|
-
{
|
|
3133
|
-
name: "id",
|
|
3134
|
-
in: "path",
|
|
3135
|
-
required: true,
|
|
3136
|
-
description: `${resourceName} ID`,
|
|
3137
|
-
schema: { type: "string" }
|
|
3138
|
-
},
|
|
3139
|
-
...hasPartitions ? [
|
|
3140
|
-
{
|
|
3141
|
-
name: "partition",
|
|
3142
|
-
in: "query",
|
|
3143
|
-
description: partitionDescription,
|
|
3144
|
-
schema: {
|
|
3145
|
-
type: "string",
|
|
3146
|
-
enum: partitionNames
|
|
3147
|
-
},
|
|
3148
|
-
example: partitionExample
|
|
3149
|
-
},
|
|
3150
|
-
{
|
|
3151
|
-
name: "partitionValues",
|
|
3152
|
-
in: "query",
|
|
3153
|
-
description: partitionValuesDescription,
|
|
3154
|
-
schema: { type: "string" },
|
|
3155
|
-
example: partitionValuesExample
|
|
3156
|
-
}
|
|
3157
|
-
] : []
|
|
3158
|
-
],
|
|
3159
|
-
responses: {
|
|
3160
|
-
200: {
|
|
3161
|
-
description: "Successful response",
|
|
3162
|
-
content: {
|
|
3163
|
-
"application/json": {
|
|
3164
|
-
schema: {
|
|
3165
|
-
type: "object",
|
|
3166
|
-
properties: {
|
|
3167
|
-
success: { type: "boolean", example: true },
|
|
3168
|
-
data: schema
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
}
|
|
3172
|
-
}
|
|
3173
|
-
},
|
|
3174
|
-
404: {
|
|
3175
|
-
description: "Resource not found",
|
|
3176
|
-
content: {
|
|
3177
|
-
"application/json": {
|
|
3178
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
3182
|
-
},
|
|
3183
|
-
security: security.length > 0 ? security : void 0
|
|
3184
|
-
}
|
|
3185
|
-
};
|
|
3186
|
-
}
|
|
3187
|
-
if (methods.includes("POST")) {
|
|
3188
|
-
if (!paths[basePath]) paths[basePath] = {};
|
|
3189
|
-
paths[basePath].post = {
|
|
3190
|
-
tags: [resourceName],
|
|
3191
|
-
summary: `Create ${resourceName}`,
|
|
3192
|
-
description: `Create a new ${resourceName}`,
|
|
3193
|
-
requestBody: {
|
|
3194
|
-
required: true,
|
|
3195
|
-
content: {
|
|
3196
|
-
"application/json": {
|
|
3197
|
-
schema
|
|
3198
|
-
}
|
|
3199
|
-
}
|
|
3200
|
-
},
|
|
3201
|
-
responses: {
|
|
3202
|
-
201: {
|
|
3203
|
-
description: "Resource created successfully",
|
|
3204
|
-
content: {
|
|
3205
|
-
"application/json": {
|
|
3206
|
-
schema: {
|
|
3207
|
-
type: "object",
|
|
3208
|
-
properties: {
|
|
3209
|
-
success: { type: "boolean", example: true },
|
|
3210
|
-
data: schema
|
|
3211
|
-
}
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
},
|
|
3215
|
-
headers: {
|
|
3216
|
-
Location: {
|
|
3217
|
-
description: "URL of the created resource",
|
|
3218
|
-
schema: { type: "string" }
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
},
|
|
3222
|
-
400: {
|
|
3223
|
-
description: "Validation error",
|
|
3224
|
-
content: {
|
|
3225
|
-
"application/json": {
|
|
3226
|
-
schema: { $ref: "#/components/schemas/ValidationError" }
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
},
|
|
3231
|
-
security: security.length > 0 ? security : void 0
|
|
3232
|
-
};
|
|
3233
|
-
}
|
|
3234
|
-
if (methods.includes("PUT")) {
|
|
3235
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3236
|
-
paths[`${basePath}/{id}`].put = {
|
|
3237
|
-
tags: [resourceName],
|
|
3238
|
-
summary: `Update ${resourceName} (full)`,
|
|
3239
|
-
description: `Fully update a ${resourceName}`,
|
|
3240
|
-
parameters: [
|
|
3241
|
-
{
|
|
3242
|
-
name: "id",
|
|
3243
|
-
in: "path",
|
|
3244
|
-
required: true,
|
|
3245
|
-
schema: { type: "string" }
|
|
3246
|
-
}
|
|
3247
|
-
],
|
|
3248
|
-
requestBody: {
|
|
3249
|
-
required: true,
|
|
3250
|
-
content: {
|
|
3251
|
-
"application/json": {
|
|
3252
|
-
schema
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
},
|
|
3256
|
-
responses: {
|
|
3257
|
-
200: {
|
|
3258
|
-
description: "Resource updated successfully",
|
|
3259
|
-
content: {
|
|
3260
|
-
"application/json": {
|
|
3261
|
-
schema: {
|
|
3262
|
-
type: "object",
|
|
3263
|
-
properties: {
|
|
3264
|
-
success: { type: "boolean", example: true },
|
|
3265
|
-
data: schema
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
}
|
|
3270
|
-
},
|
|
3271
|
-
404: {
|
|
3272
|
-
description: "Resource not found",
|
|
3273
|
-
content: {
|
|
3274
|
-
"application/json": {
|
|
3275
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3276
|
-
}
|
|
3277
|
-
}
|
|
3278
|
-
}
|
|
3279
|
-
},
|
|
3280
|
-
security: security.length > 0 ? security : void 0
|
|
3281
|
-
};
|
|
3282
|
-
}
|
|
3283
|
-
if (methods.includes("PATCH")) {
|
|
3284
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3285
|
-
paths[`${basePath}/{id}`].patch = {
|
|
3286
|
-
tags: [resourceName],
|
|
3287
|
-
summary: `Update ${resourceName} (partial)`,
|
|
3288
|
-
description: `Partially update a ${resourceName}`,
|
|
3289
|
-
parameters: [
|
|
3290
|
-
{
|
|
3291
|
-
name: "id",
|
|
3292
|
-
in: "path",
|
|
3293
|
-
required: true,
|
|
3294
|
-
schema: { type: "string" }
|
|
3295
|
-
}
|
|
3296
|
-
],
|
|
3297
|
-
requestBody: {
|
|
3298
|
-
required: true,
|
|
3299
|
-
content: {
|
|
3300
|
-
"application/json": {
|
|
3301
|
-
schema: {
|
|
3302
|
-
...schema,
|
|
3303
|
-
required: void 0
|
|
3304
|
-
// Partial updates don't require all fields
|
|
3305
|
-
}
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
},
|
|
3309
|
-
responses: {
|
|
3310
|
-
200: {
|
|
3311
|
-
description: "Resource updated successfully",
|
|
3312
|
-
content: {
|
|
3313
|
-
"application/json": {
|
|
3314
|
-
schema: {
|
|
3315
|
-
type: "object",
|
|
3316
|
-
properties: {
|
|
3317
|
-
success: { type: "boolean", example: true },
|
|
3318
|
-
data: schema
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
}
|
|
3322
|
-
}
|
|
3323
|
-
},
|
|
3324
|
-
404: {
|
|
3325
|
-
description: "Resource not found",
|
|
3326
|
-
content: {
|
|
3327
|
-
"application/json": {
|
|
3328
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
}
|
|
3332
|
-
},
|
|
3333
|
-
security: security.length > 0 ? security : void 0
|
|
3334
|
-
};
|
|
3335
|
-
}
|
|
3336
|
-
if (methods.includes("DELETE")) {
|
|
3337
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3338
|
-
paths[`${basePath}/{id}`].delete = {
|
|
3339
|
-
tags: [resourceName],
|
|
3340
|
-
summary: `Delete ${resourceName}`,
|
|
3341
|
-
description: `Delete a ${resourceName} by ID`,
|
|
3342
|
-
parameters: [
|
|
3343
|
-
{
|
|
3344
|
-
name: "id",
|
|
3345
|
-
in: "path",
|
|
3346
|
-
required: true,
|
|
3347
|
-
schema: { type: "string" }
|
|
3348
|
-
}
|
|
3349
|
-
],
|
|
3350
|
-
responses: {
|
|
3351
|
-
204: {
|
|
3352
|
-
description: "Resource deleted successfully"
|
|
3353
|
-
},
|
|
3354
|
-
404: {
|
|
3355
|
-
description: "Resource not found",
|
|
3356
|
-
content: {
|
|
3357
|
-
"application/json": {
|
|
3358
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
3361
|
-
}
|
|
3362
|
-
},
|
|
3363
|
-
security: security.length > 0 ? security : void 0
|
|
3364
|
-
};
|
|
3365
|
-
}
|
|
3366
|
-
if (methods.includes("HEAD")) {
|
|
3367
|
-
if (!paths[basePath]) paths[basePath] = {};
|
|
3368
|
-
paths[basePath].head = {
|
|
3369
|
-
tags: [resourceName],
|
|
3370
|
-
summary: `Get ${resourceName} statistics`,
|
|
3371
|
-
description: `Get statistics about ${resourceName} collection without retrieving data. Returns statistics in response headers.`,
|
|
3372
|
-
responses: {
|
|
3373
|
-
200: {
|
|
3374
|
-
description: "Statistics retrieved successfully",
|
|
3375
|
-
headers: {
|
|
3376
|
-
"X-Total-Count": {
|
|
3377
|
-
description: "Total number of records",
|
|
3378
|
-
schema: { type: "integer" }
|
|
3379
|
-
},
|
|
3380
|
-
"X-Resource-Version": {
|
|
3381
|
-
description: "Current resource version",
|
|
3382
|
-
schema: { type: "string" }
|
|
3383
|
-
},
|
|
3384
|
-
"X-Schema-Fields": {
|
|
3385
|
-
description: "Number of schema fields",
|
|
3386
|
-
schema: { type: "integer" }
|
|
3387
|
-
}
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
},
|
|
3391
|
-
security: security.length > 0 ? security : void 0
|
|
3392
|
-
};
|
|
3393
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3394
|
-
paths[`${basePath}/{id}`].head = {
|
|
3395
|
-
tags: [resourceName],
|
|
3396
|
-
summary: `Check if ${resourceName} exists`,
|
|
3397
|
-
description: `Check if a ${resourceName} exists without retrieving its data`,
|
|
3398
|
-
parameters: [
|
|
3399
|
-
{
|
|
3400
|
-
name: "id",
|
|
3401
|
-
in: "path",
|
|
3402
|
-
required: true,
|
|
3403
|
-
schema: { type: "string" }
|
|
3404
|
-
}
|
|
3405
|
-
],
|
|
3406
|
-
responses: {
|
|
3407
|
-
200: {
|
|
3408
|
-
description: "Resource exists",
|
|
3409
|
-
headers: {
|
|
3410
|
-
"Last-Modified": {
|
|
3411
|
-
description: "Last modification date",
|
|
3412
|
-
schema: { type: "string", format: "date-time" }
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
},
|
|
3416
|
-
404: {
|
|
3417
|
-
description: "Resource not found"
|
|
3418
|
-
}
|
|
3419
|
-
},
|
|
3420
|
-
security: security.length > 0 ? security : void 0
|
|
3421
|
-
};
|
|
3422
|
-
}
|
|
3423
|
-
if (methods.includes("OPTIONS")) {
|
|
3424
|
-
if (!paths[basePath]) paths[basePath] = {};
|
|
3425
|
-
paths[basePath].options = {
|
|
3426
|
-
tags: [resourceName],
|
|
3427
|
-
summary: `Get ${resourceName} metadata`,
|
|
3428
|
-
description: `Get complete metadata about ${resourceName} resource including schema, allowed methods, endpoints, and query parameters`,
|
|
3429
|
-
responses: {
|
|
3430
|
-
200: {
|
|
3431
|
-
description: "Metadata retrieved successfully",
|
|
3432
|
-
headers: {
|
|
3433
|
-
"Allow": {
|
|
3434
|
-
description: "Allowed HTTP methods",
|
|
3435
|
-
schema: { type: "string", example: "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS" }
|
|
3436
|
-
}
|
|
3437
|
-
},
|
|
3438
|
-
content: {
|
|
3439
|
-
"application/json": {
|
|
3440
|
-
schema: {
|
|
3441
|
-
type: "object",
|
|
3442
|
-
properties: {
|
|
3443
|
-
resource: { type: "string" },
|
|
3444
|
-
version: { type: "string" },
|
|
3445
|
-
totalRecords: { type: "integer" },
|
|
3446
|
-
allowedMethods: {
|
|
3447
|
-
type: "array",
|
|
3448
|
-
items: { type: "string" }
|
|
3449
|
-
},
|
|
3450
|
-
schema: {
|
|
3451
|
-
type: "array",
|
|
3452
|
-
items: {
|
|
3453
|
-
type: "object",
|
|
3454
|
-
properties: {
|
|
3455
|
-
name: { type: "string" },
|
|
3456
|
-
type: { type: "string" },
|
|
3457
|
-
rules: { type: "array", items: { type: "string" } }
|
|
3458
|
-
}
|
|
3459
|
-
}
|
|
3460
|
-
},
|
|
3461
|
-
endpoints: {
|
|
3462
|
-
type: "object",
|
|
3463
|
-
properties: {
|
|
3464
|
-
list: { type: "string" },
|
|
3465
|
-
get: { type: "string" },
|
|
3466
|
-
create: { type: "string" },
|
|
3467
|
-
update: { type: "string" },
|
|
3468
|
-
delete: { type: "string" }
|
|
3469
|
-
}
|
|
3470
|
-
},
|
|
3471
|
-
queryParameters: { type: "object" }
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
}
|
|
3475
|
-
}
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
};
|
|
3479
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3480
|
-
paths[`${basePath}/{id}`].options = {
|
|
3481
|
-
tags: [resourceName],
|
|
3482
|
-
summary: `Get allowed methods for ${resourceName} item`,
|
|
3483
|
-
description: `Get allowed HTTP methods for individual ${resourceName} operations`,
|
|
3484
|
-
parameters: [
|
|
3485
|
-
{
|
|
3486
|
-
name: "id",
|
|
3487
|
-
in: "path",
|
|
3488
|
-
required: true,
|
|
3489
|
-
schema: { type: "string" }
|
|
3490
|
-
}
|
|
3491
|
-
],
|
|
3492
|
-
responses: {
|
|
3493
|
-
204: {
|
|
3494
|
-
description: "Methods retrieved successfully",
|
|
3495
|
-
headers: {
|
|
3496
|
-
"Allow": {
|
|
3497
|
-
description: "Allowed HTTP methods",
|
|
3498
|
-
schema: { type: "string", example: "GET, PUT, PATCH, DELETE, HEAD, OPTIONS" }
|
|
3499
|
-
}
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
};
|
|
3504
|
-
}
|
|
3505
|
-
return paths;
|
|
3506
|
-
}
|
|
3507
|
-
function generateOpenAPISpec(database, config = {}) {
|
|
3508
|
-
const {
|
|
3509
|
-
title = "s3db.js API",
|
|
3510
|
-
version = "1.0.0",
|
|
3511
|
-
description = "Auto-generated REST API documentation for s3db.js resources",
|
|
3512
|
-
serverUrl = "http://localhost:3000",
|
|
3513
|
-
auth = {},
|
|
3514
|
-
resources: resourceConfigs = {}
|
|
3515
|
-
} = config;
|
|
3516
|
-
const spec = {
|
|
3517
|
-
openapi: "3.1.0",
|
|
3518
|
-
info: {
|
|
3519
|
-
title,
|
|
3520
|
-
version,
|
|
3521
|
-
description,
|
|
3522
|
-
contact: {
|
|
3523
|
-
name: "s3db.js",
|
|
3524
|
-
url: "https://github.com/forattini-dev/s3db.js"
|
|
3525
|
-
}
|
|
3526
|
-
},
|
|
3527
|
-
servers: [
|
|
3528
|
-
{
|
|
3529
|
-
url: serverUrl,
|
|
3530
|
-
description: "API Server"
|
|
3531
|
-
}
|
|
3532
|
-
],
|
|
3533
|
-
paths: {},
|
|
3534
|
-
components: {
|
|
3535
|
-
schemas: {
|
|
3536
|
-
Error: {
|
|
3537
|
-
type: "object",
|
|
3538
|
-
properties: {
|
|
3539
|
-
success: { type: "boolean", example: false },
|
|
3540
|
-
error: {
|
|
3541
|
-
type: "object",
|
|
3542
|
-
properties: {
|
|
3543
|
-
message: { type: "string" },
|
|
3544
|
-
code: { type: "string" },
|
|
3545
|
-
details: { type: "object" }
|
|
3546
|
-
}
|
|
3547
|
-
}
|
|
3548
|
-
}
|
|
3549
|
-
},
|
|
3550
|
-
ValidationError: {
|
|
3551
|
-
type: "object",
|
|
3552
|
-
properties: {
|
|
3553
|
-
success: { type: "boolean", example: false },
|
|
3554
|
-
error: {
|
|
3555
|
-
type: "object",
|
|
3556
|
-
properties: {
|
|
3557
|
-
message: { type: "string", example: "Validation failed" },
|
|
3558
|
-
code: { type: "string", example: "VALIDATION_ERROR" },
|
|
3559
|
-
details: {
|
|
3560
|
-
type: "object",
|
|
3561
|
-
properties: {
|
|
3562
|
-
errors: {
|
|
3563
|
-
type: "array",
|
|
3564
|
-
items: {
|
|
3565
|
-
type: "object",
|
|
3566
|
-
properties: {
|
|
3567
|
-
field: { type: "string" },
|
|
3568
|
-
message: { type: "string" },
|
|
3569
|
-
expected: { type: "string" },
|
|
3570
|
-
actual: {}
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
}
|
|
3574
|
-
}
|
|
3575
|
-
}
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
},
|
|
3581
|
-
securitySchemes: {}
|
|
3582
|
-
},
|
|
3583
|
-
tags: []
|
|
3584
|
-
};
|
|
3585
|
-
if (auth.jwt?.enabled) {
|
|
3586
|
-
spec.components.securitySchemes.bearerAuth = {
|
|
3587
|
-
type: "http",
|
|
3588
|
-
scheme: "bearer",
|
|
3589
|
-
bearerFormat: "JWT",
|
|
3590
|
-
description: "JWT authentication"
|
|
3591
|
-
};
|
|
3592
|
-
}
|
|
3593
|
-
if (auth.apiKey?.enabled) {
|
|
3594
|
-
spec.components.securitySchemes.apiKeyAuth = {
|
|
3595
|
-
type: "apiKey",
|
|
3596
|
-
in: "header",
|
|
3597
|
-
name: auth.apiKey.headerName || "X-API-Key",
|
|
3598
|
-
description: "API Key authentication"
|
|
3599
|
-
};
|
|
3600
|
-
}
|
|
3601
|
-
if (auth.basic?.enabled) {
|
|
3602
|
-
spec.components.securitySchemes.basicAuth = {
|
|
3603
|
-
type: "http",
|
|
3604
|
-
scheme: "basic",
|
|
3605
|
-
description: "HTTP Basic authentication"
|
|
3606
|
-
};
|
|
3607
|
-
}
|
|
3608
|
-
const resources = database.resources;
|
|
3609
|
-
for (const [name, resource] of Object.entries(resources)) {
|
|
3610
|
-
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
3611
|
-
continue;
|
|
3612
|
-
}
|
|
3613
|
-
const config2 = resourceConfigs[name] || {
|
|
3614
|
-
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
3615
|
-
auth: false
|
|
3616
|
-
};
|
|
3617
|
-
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
3618
|
-
const paths = generateResourcePaths(resource, version2, config2);
|
|
3619
|
-
Object.assign(spec.paths, paths);
|
|
3620
|
-
spec.tags.push({
|
|
3621
|
-
name,
|
|
3622
|
-
description: `Operations for ${name} resource`
|
|
3623
|
-
});
|
|
3624
|
-
spec.components.schemas[name] = generateResourceSchema(resource);
|
|
3625
|
-
}
|
|
3626
|
-
if (auth.jwt?.enabled || auth.apiKey?.enabled || auth.basic?.enabled) {
|
|
3627
|
-
spec.paths["/auth/login"] = {
|
|
3628
|
-
post: {
|
|
3629
|
-
tags: ["Authentication"],
|
|
3630
|
-
summary: "Login",
|
|
3631
|
-
description: "Authenticate with username and password",
|
|
3632
|
-
requestBody: {
|
|
3633
|
-
required: true,
|
|
3634
|
-
content: {
|
|
3635
|
-
"application/json": {
|
|
3636
|
-
schema: {
|
|
3637
|
-
type: "object",
|
|
3638
|
-
properties: {
|
|
3639
|
-
username: { type: "string" },
|
|
3640
|
-
password: { type: "string", format: "password" }
|
|
3641
|
-
},
|
|
3642
|
-
required: ["username", "password"]
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3645
|
-
}
|
|
3646
|
-
},
|
|
3647
|
-
responses: {
|
|
3648
|
-
200: {
|
|
3649
|
-
description: "Login successful",
|
|
3650
|
-
content: {
|
|
3651
|
-
"application/json": {
|
|
3652
|
-
schema: {
|
|
3653
|
-
type: "object",
|
|
3654
|
-
properties: {
|
|
3655
|
-
success: { type: "boolean", example: true },
|
|
3656
|
-
data: {
|
|
3657
|
-
type: "object",
|
|
3658
|
-
properties: {
|
|
3659
|
-
token: { type: "string" },
|
|
3660
|
-
user: { type: "object" }
|
|
3661
|
-
}
|
|
3662
|
-
}
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3665
|
-
}
|
|
3666
|
-
}
|
|
3667
|
-
},
|
|
3668
|
-
401: {
|
|
3669
|
-
description: "Invalid credentials",
|
|
3670
|
-
content: {
|
|
3671
|
-
"application/json": {
|
|
3672
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3676
|
-
}
|
|
3677
|
-
}
|
|
3678
|
-
};
|
|
3679
|
-
spec.paths["/auth/register"] = {
|
|
3680
|
-
post: {
|
|
3681
|
-
tags: ["Authentication"],
|
|
3682
|
-
summary: "Register",
|
|
3683
|
-
description: "Register a new user",
|
|
3684
|
-
requestBody: {
|
|
3685
|
-
required: true,
|
|
3686
|
-
content: {
|
|
3687
|
-
"application/json": {
|
|
3688
|
-
schema: {
|
|
3689
|
-
type: "object",
|
|
3690
|
-
properties: {
|
|
3691
|
-
username: { type: "string", minLength: 3 },
|
|
3692
|
-
password: { type: "string", format: "password", minLength: 8 },
|
|
3693
|
-
email: { type: "string", format: "email" }
|
|
3694
|
-
},
|
|
3695
|
-
required: ["username", "password"]
|
|
3696
|
-
}
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
},
|
|
3700
|
-
responses: {
|
|
3701
|
-
201: {
|
|
3702
|
-
description: "User registered successfully",
|
|
3703
|
-
content: {
|
|
3704
|
-
"application/json": {
|
|
3705
|
-
schema: {
|
|
3706
|
-
type: "object",
|
|
3707
|
-
properties: {
|
|
3708
|
-
success: { type: "boolean", example: true },
|
|
3709
|
-
data: {
|
|
3710
|
-
type: "object",
|
|
3711
|
-
properties: {
|
|
3712
|
-
token: { type: "string" },
|
|
3713
|
-
user: { type: "object" }
|
|
3714
|
-
}
|
|
3715
|
-
}
|
|
3716
|
-
}
|
|
3717
|
-
}
|
|
3718
|
-
}
|
|
3719
|
-
}
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
|
-
};
|
|
3724
|
-
spec.tags.push({
|
|
3725
|
-
name: "Authentication",
|
|
3726
|
-
description: "Authentication endpoints"
|
|
3727
|
-
});
|
|
3728
|
-
}
|
|
3729
|
-
spec.paths["/health"] = {
|
|
3730
|
-
get: {
|
|
3731
|
-
tags: ["Health"],
|
|
3732
|
-
summary: "Generic Health Check",
|
|
3733
|
-
description: "Generic health check endpoint that includes references to liveness and readiness probes",
|
|
3734
|
-
responses: {
|
|
3735
|
-
200: {
|
|
3736
|
-
description: "API is healthy",
|
|
3737
|
-
content: {
|
|
3738
|
-
"application/json": {
|
|
3739
|
-
schema: {
|
|
3740
|
-
type: "object",
|
|
3741
|
-
properties: {
|
|
3742
|
-
success: { type: "boolean", example: true },
|
|
3743
|
-
data: {
|
|
3744
|
-
type: "object",
|
|
3745
|
-
properties: {
|
|
3746
|
-
status: { type: "string", example: "ok" },
|
|
3747
|
-
uptime: { type: "number", description: "Process uptime in seconds" },
|
|
3748
|
-
timestamp: { type: "string", format: "date-time" },
|
|
3749
|
-
checks: {
|
|
3750
|
-
type: "object",
|
|
3751
|
-
properties: {
|
|
3752
|
-
liveness: { type: "string", example: "/health/live" },
|
|
3753
|
-
readiness: { type: "string", example: "/health/ready" }
|
|
3754
|
-
}
|
|
3755
|
-
}
|
|
3756
|
-
}
|
|
3757
|
-
}
|
|
3758
|
-
}
|
|
3759
|
-
}
|
|
3760
|
-
}
|
|
3761
|
-
}
|
|
3762
|
-
}
|
|
3763
|
-
}
|
|
3764
|
-
}
|
|
3765
|
-
};
|
|
3766
|
-
spec.paths["/health/live"] = {
|
|
3767
|
-
get: {
|
|
3768
|
-
tags: ["Health"],
|
|
3769
|
-
summary: "Liveness Probe",
|
|
3770
|
-
description: "Kubernetes liveness probe - checks if the application is alive. If this fails, Kubernetes will restart the pod.",
|
|
3771
|
-
responses: {
|
|
3772
|
-
200: {
|
|
3773
|
-
description: "Application is alive",
|
|
3774
|
-
content: {
|
|
3775
|
-
"application/json": {
|
|
3776
|
-
schema: {
|
|
3777
|
-
type: "object",
|
|
3778
|
-
properties: {
|
|
3779
|
-
success: { type: "boolean", example: true },
|
|
3780
|
-
data: {
|
|
3781
|
-
type: "object",
|
|
3782
|
-
properties: {
|
|
3783
|
-
status: { type: "string", example: "alive" },
|
|
3784
|
-
timestamp: { type: "string", format: "date-time" }
|
|
3785
|
-
}
|
|
3786
|
-
}
|
|
3787
|
-
}
|
|
3788
|
-
}
|
|
3789
|
-
}
|
|
3790
|
-
}
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
|
-
}
|
|
3794
|
-
};
|
|
3795
|
-
spec.paths["/health/ready"] = {
|
|
3796
|
-
get: {
|
|
3797
|
-
tags: ["Health"],
|
|
3798
|
-
summary: "Readiness Probe",
|
|
3799
|
-
description: "Kubernetes readiness probe - checks if the application is ready to receive traffic. If this fails, Kubernetes will remove the pod from service endpoints.",
|
|
3800
|
-
responses: {
|
|
3801
|
-
200: {
|
|
3802
|
-
description: "Application is ready to receive traffic",
|
|
3803
|
-
content: {
|
|
3804
|
-
"application/json": {
|
|
3805
|
-
schema: {
|
|
3806
|
-
type: "object",
|
|
3807
|
-
properties: {
|
|
3808
|
-
success: { type: "boolean", example: true },
|
|
3809
|
-
data: {
|
|
3810
|
-
type: "object",
|
|
3811
|
-
properties: {
|
|
3812
|
-
status: { type: "string", example: "ready" },
|
|
3813
|
-
database: {
|
|
3814
|
-
type: "object",
|
|
3815
|
-
properties: {
|
|
3816
|
-
connected: { type: "boolean", example: true },
|
|
3817
|
-
resources: { type: "integer", example: 5 }
|
|
3818
|
-
}
|
|
3819
|
-
},
|
|
3820
|
-
timestamp: { type: "string", format: "date-time" }
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
}
|
|
3824
|
-
}
|
|
3825
|
-
}
|
|
3826
|
-
}
|
|
3827
|
-
},
|
|
3828
|
-
503: {
|
|
3829
|
-
description: "Application is not ready",
|
|
3830
|
-
content: {
|
|
3831
|
-
"application/json": {
|
|
3832
|
-
schema: {
|
|
3833
|
-
type: "object",
|
|
3834
|
-
properties: {
|
|
3835
|
-
success: { type: "boolean", example: false },
|
|
3836
|
-
error: {
|
|
3837
|
-
type: "object",
|
|
3838
|
-
properties: {
|
|
3839
|
-
message: { type: "string", example: "Service not ready" },
|
|
3840
|
-
code: { type: "string", example: "NOT_READY" },
|
|
3841
|
-
details: {
|
|
3842
|
-
type: "object",
|
|
3843
|
-
properties: {
|
|
3844
|
-
database: {
|
|
3845
|
-
type: "object",
|
|
3846
|
-
properties: {
|
|
3847
|
-
connected: { type: "boolean", example: false },
|
|
3848
|
-
resources: { type: "integer", example: 0 }
|
|
3849
|
-
}
|
|
3850
|
-
}
|
|
3851
|
-
}
|
|
3852
|
-
}
|
|
3853
|
-
}
|
|
3854
|
-
}
|
|
3855
|
-
}
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3858
|
-
}
|
|
3859
|
-
}
|
|
3860
|
-
}
|
|
3861
|
-
}
|
|
3862
|
-
};
|
|
3863
|
-
spec.tags.push({
|
|
3864
|
-
name: "Health",
|
|
3865
|
-
description: "Health check endpoints for monitoring and Kubernetes probes"
|
|
3866
|
-
});
|
|
3867
|
-
const metricsPlugin = database.plugins?.metrics || database.plugins?.MetricsPlugin;
|
|
3868
|
-
if (metricsPlugin && metricsPlugin.config?.prometheus?.enabled) {
|
|
3869
|
-
const metricsPath = metricsPlugin.config.prometheus.path || "/metrics";
|
|
3870
|
-
const isIntegrated = metricsPlugin.config.prometheus.mode !== "standalone";
|
|
3871
|
-
if (isIntegrated) {
|
|
3872
|
-
spec.paths[metricsPath] = {
|
|
3873
|
-
get: {
|
|
3874
|
-
tags: ["Monitoring"],
|
|
3875
|
-
summary: "Prometheus Metrics",
|
|
3876
|
-
description: "Exposes application metrics in Prometheus text-based exposition format for monitoring and observability. Metrics include operation counts, durations, errors, uptime, and resource statistics.",
|
|
3877
|
-
responses: {
|
|
3878
|
-
200: {
|
|
3879
|
-
description: "Metrics in Prometheus format",
|
|
3880
|
-
content: {
|
|
3881
|
-
"text/plain": {
|
|
3882
|
-
schema: {
|
|
3883
|
-
type: "string",
|
|
3884
|
-
example: '# HELP s3db_operations_total Total number of operations by type and resource\n# TYPE s3db_operations_total counter\ns3db_operations_total{operation="insert",resource="cars"} 1523\ns3db_operations_total{operation="update",resource="cars"} 342\n\n# HELP s3db_operation_duration_seconds Average operation duration in seconds\n# TYPE s3db_operation_duration_seconds gauge\ns3db_operation_duration_seconds{operation="insert",resource="cars"} 0.045\n\n# HELP s3db_operation_errors_total Total number of operation errors\n# TYPE s3db_operation_errors_total counter\ns3db_operation_errors_total{operation="insert",resource="cars"} 12\n'
|
|
3885
|
-
}
|
|
3886
|
-
}
|
|
3887
|
-
}
|
|
3888
|
-
}
|
|
3889
|
-
}
|
|
3890
|
-
}
|
|
3891
|
-
};
|
|
3892
|
-
spec.tags.push({
|
|
3893
|
-
name: "Monitoring",
|
|
3894
|
-
description: "Monitoring and observability endpoints (Prometheus)"
|
|
3895
|
-
});
|
|
3896
|
-
}
|
|
3897
|
-
}
|
|
3898
|
-
return spec;
|
|
3899
|
-
}
|
|
3900
|
-
|
|
3901
|
-
class ApiServer {
|
|
3902
|
-
/**
|
|
3903
|
-
* Create API server
|
|
3904
|
-
* @param {Object} options - Server options
|
|
3905
|
-
* @param {number} options.port - Server port
|
|
3906
|
-
* @param {string} options.host - Server host
|
|
3907
|
-
* @param {Object} options.database - s3db.js database instance
|
|
3908
|
-
* @param {Object} options.resources - Resource configuration
|
|
3909
|
-
* @param {Array} options.middlewares - Global middlewares
|
|
3910
|
-
*/
|
|
3911
|
-
constructor(options = {}) {
|
|
3912
|
-
this.options = {
|
|
3913
|
-
port: options.port || 3e3,
|
|
3914
|
-
host: options.host || "0.0.0.0",
|
|
3915
|
-
database: options.database,
|
|
3916
|
-
resources: options.resources || {},
|
|
3917
|
-
middlewares: options.middlewares || [],
|
|
3918
|
-
verbose: options.verbose || false,
|
|
3919
|
-
auth: options.auth || {},
|
|
3920
|
-
docsEnabled: options.docsEnabled !== false,
|
|
3921
|
-
// Enable /docs by default
|
|
3922
|
-
docsUI: options.docsUI || "redoc",
|
|
3923
|
-
// 'swagger' or 'redoc'
|
|
3924
|
-
maxBodySize: options.maxBodySize || 10 * 1024 * 1024,
|
|
3925
|
-
// 10MB default
|
|
3926
|
-
rootHandler: options.rootHandler,
|
|
3927
|
-
// Custom handler for root path, if not provided redirects to /docs
|
|
3928
|
-
apiInfo: {
|
|
3929
|
-
title: options.apiTitle || "s3db.js API",
|
|
3930
|
-
version: options.apiVersion || "1.0.0",
|
|
3931
|
-
description: options.apiDescription || "Auto-generated REST API for s3db.js resources"
|
|
3932
|
-
}
|
|
3933
|
-
};
|
|
3934
|
-
this.app = new Hono();
|
|
3935
|
-
this.server = null;
|
|
3936
|
-
this.isRunning = false;
|
|
3937
|
-
this.openAPISpec = null;
|
|
3938
|
-
this._setupRoutes();
|
|
3939
|
-
}
|
|
3940
|
-
/**
|
|
3941
|
-
* Setup all routes
|
|
3942
|
-
* @private
|
|
3943
|
-
*/
|
|
3944
|
-
_setupRoutes() {
|
|
3945
|
-
this.options.middlewares.forEach((middleware) => {
|
|
3946
|
-
this.app.use("*", middleware);
|
|
3947
|
-
});
|
|
3948
|
-
this.app.use("*", async (c, next) => {
|
|
3949
|
-
const method = c.req.method;
|
|
3950
|
-
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
3951
|
-
const contentLength = c.req.header("content-length");
|
|
3952
|
-
if (contentLength) {
|
|
3953
|
-
const size = parseInt(contentLength);
|
|
3954
|
-
if (size > this.options.maxBodySize) {
|
|
3955
|
-
const response = payloadTooLarge(size, this.options.maxBodySize);
|
|
3956
|
-
c.header("Connection", "close");
|
|
3957
|
-
return c.json(response, response._status);
|
|
3958
|
-
}
|
|
3959
|
-
}
|
|
3960
|
-
}
|
|
3961
|
-
await next();
|
|
3962
|
-
});
|
|
3963
|
-
this.app.get("/health/live", (c) => {
|
|
3964
|
-
const response = success({
|
|
3965
|
-
status: "alive",
|
|
3966
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3967
|
-
});
|
|
3968
|
-
return c.json(response);
|
|
3969
|
-
});
|
|
3970
|
-
this.app.get("/health/ready", (c) => {
|
|
3971
|
-
const isReady = this.options.database && this.options.database.connected && Object.keys(this.options.database.resources).length > 0;
|
|
3972
|
-
if (!isReady) {
|
|
3973
|
-
const response2 = error("Service not ready", {
|
|
3974
|
-
status: 503,
|
|
3975
|
-
code: "NOT_READY",
|
|
3976
|
-
details: {
|
|
3977
|
-
database: {
|
|
3978
|
-
connected: this.options.database?.connected || false,
|
|
3979
|
-
resources: Object.keys(this.options.database?.resources || {}).length
|
|
3980
|
-
}
|
|
3981
|
-
}
|
|
3982
|
-
});
|
|
3983
|
-
return c.json(response2, 503);
|
|
3984
|
-
}
|
|
3985
|
-
const response = success({
|
|
3986
|
-
status: "ready",
|
|
3987
|
-
database: {
|
|
3988
|
-
connected: true,
|
|
3989
|
-
resources: Object.keys(this.options.database.resources).length
|
|
3990
|
-
},
|
|
3991
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3992
|
-
});
|
|
3993
|
-
return c.json(response);
|
|
3994
|
-
});
|
|
3995
|
-
this.app.get("/health", (c) => {
|
|
3996
|
-
const response = success({
|
|
3997
|
-
status: "ok",
|
|
3998
|
-
uptime: process.uptime(),
|
|
3999
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4000
|
-
checks: {
|
|
4001
|
-
liveness: "/health/live",
|
|
4002
|
-
readiness: "/health/ready"
|
|
4003
|
-
}
|
|
4004
|
-
});
|
|
4005
|
-
return c.json(response);
|
|
4006
|
-
});
|
|
4007
|
-
this.app.get("/", (c) => {
|
|
4008
|
-
if (this.options.rootHandler) {
|
|
4009
|
-
return this.options.rootHandler(c);
|
|
4010
|
-
}
|
|
4011
|
-
return c.redirect("/docs", 302);
|
|
4012
|
-
});
|
|
4013
|
-
if (this.options.docsEnabled) {
|
|
4014
|
-
this.app.get("/openapi.json", (c) => {
|
|
4015
|
-
if (!this.openAPISpec) {
|
|
4016
|
-
this.openAPISpec = this._generateOpenAPISpec();
|
|
4017
|
-
}
|
|
4018
|
-
return c.json(this.openAPISpec);
|
|
4019
|
-
});
|
|
4020
|
-
if (this.options.docsUI === "swagger") {
|
|
4021
|
-
this.app.get("/docs", swaggerUI({
|
|
4022
|
-
url: "/openapi.json"
|
|
4023
|
-
}));
|
|
4024
|
-
} else {
|
|
4025
|
-
this.app.get("/docs", (c) => {
|
|
4026
|
-
return c.html(`<!DOCTYPE html>
|
|
4027
|
-
<html lang="en">
|
|
4028
|
-
<head>
|
|
4029
|
-
<meta charset="UTF-8">
|
|
4030
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4031
|
-
<title>${this.options.apiInfo.title} - API Documentation</title>
|
|
4032
|
-
<style>
|
|
4033
|
-
body {
|
|
4034
|
-
margin: 0;
|
|
4035
|
-
padding: 0;
|
|
4036
|
-
}
|
|
4037
|
-
</style>
|
|
4038
|
-
</head>
|
|
4039
|
-
<body>
|
|
4040
|
-
<redoc spec-url="/openapi.json"></redoc>
|
|
4041
|
-
<script src="https://cdn.redoc.ly/redoc/v2.5.1/bundles/redoc.standalone.js"><\/script>
|
|
4042
|
-
</body>
|
|
4043
|
-
</html>`);
|
|
4044
|
-
});
|
|
4045
|
-
}
|
|
4046
|
-
}
|
|
4047
|
-
this._setupResourceRoutes();
|
|
4048
|
-
this.app.onError((err, c) => {
|
|
4049
|
-
return errorHandler(err, c);
|
|
4050
|
-
});
|
|
4051
|
-
this.app.notFound((c) => {
|
|
4052
|
-
const response = error("Route not found", {
|
|
4053
|
-
status: 404,
|
|
4054
|
-
code: "NOT_FOUND",
|
|
4055
|
-
details: {
|
|
4056
|
-
path: c.req.path,
|
|
4057
|
-
method: c.req.method
|
|
4058
|
-
}
|
|
4059
|
-
});
|
|
4060
|
-
return c.json(response, 404);
|
|
4061
|
-
});
|
|
4062
|
-
}
|
|
4063
|
-
/**
|
|
4064
|
-
* Setup routes for all resources
|
|
4065
|
-
* @private
|
|
4066
|
-
*/
|
|
4067
|
-
_setupResourceRoutes() {
|
|
4068
|
-
const { database, resources: resourceConfigs } = this.options;
|
|
4069
|
-
const resources = database.resources;
|
|
4070
|
-
for (const [name, resource] of Object.entries(resources)) {
|
|
4071
|
-
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
4072
|
-
continue;
|
|
4073
|
-
}
|
|
4074
|
-
const config = resourceConfigs[name] || {
|
|
4075
|
-
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]};
|
|
4076
|
-
const version = resource.config?.currentVersion || resource.version || "v1";
|
|
4077
|
-
const resourceApp = createResourceRoutes(resource, version, {
|
|
4078
|
-
methods: config.methods,
|
|
4079
|
-
customMiddleware: config.customMiddleware || [],
|
|
4080
|
-
enableValidation: config.validation !== false
|
|
4081
|
-
});
|
|
4082
|
-
this.app.route(`/${version}/${name}`, resourceApp);
|
|
4083
|
-
if (this.options.verbose) {
|
|
4084
|
-
console.log(`[API Plugin] Mounted routes for resource '${name}' at /${version}/${name}`);
|
|
4085
|
-
}
|
|
4086
|
-
}
|
|
4087
|
-
}
|
|
4088
|
-
/**
|
|
4089
|
-
* Start the server
|
|
4090
|
-
* @returns {Promise<void>}
|
|
4091
|
-
*/
|
|
4092
|
-
async start() {
|
|
4093
|
-
if (this.isRunning) {
|
|
4094
|
-
console.warn("[API Plugin] Server is already running");
|
|
4095
|
-
return;
|
|
4096
|
-
}
|
|
4097
|
-
const { port, host } = this.options;
|
|
4098
|
-
return new Promise((resolve, reject) => {
|
|
4099
|
-
try {
|
|
4100
|
-
this.server = serve({
|
|
4101
|
-
fetch: this.app.fetch,
|
|
4102
|
-
port,
|
|
4103
|
-
hostname: host
|
|
4104
|
-
}, (info) => {
|
|
4105
|
-
this.isRunning = true;
|
|
4106
|
-
console.log(`[API Plugin] Server listening on http://${info.address}:${info.port}`);
|
|
4107
|
-
resolve();
|
|
4108
|
-
});
|
|
4109
|
-
} catch (err) {
|
|
4110
|
-
reject(err);
|
|
4111
|
-
}
|
|
4112
|
-
});
|
|
4113
|
-
}
|
|
4114
|
-
/**
|
|
4115
|
-
* Stop the server
|
|
4116
|
-
* @returns {Promise<void>}
|
|
4117
|
-
*/
|
|
4118
|
-
async stop() {
|
|
4119
|
-
if (!this.isRunning) {
|
|
4120
|
-
console.warn("[API Plugin] Server is not running");
|
|
4121
|
-
return;
|
|
4122
|
-
}
|
|
4123
|
-
if (this.server && typeof this.server.close === "function") {
|
|
4124
|
-
await new Promise((resolve) => {
|
|
4125
|
-
this.server.close(() => {
|
|
4126
|
-
this.isRunning = false;
|
|
4127
|
-
console.log("[API Plugin] Server stopped");
|
|
4128
|
-
resolve();
|
|
4129
|
-
});
|
|
4130
|
-
});
|
|
4131
|
-
} else {
|
|
4132
|
-
this.isRunning = false;
|
|
4133
|
-
console.log("[API Plugin] Server stopped");
|
|
4134
|
-
}
|
|
4135
|
-
}
|
|
4136
|
-
/**
|
|
4137
|
-
* Get server info
|
|
4138
|
-
* @returns {Object} Server information
|
|
4139
|
-
*/
|
|
4140
|
-
getInfo() {
|
|
4141
|
-
return {
|
|
4142
|
-
isRunning: this.isRunning,
|
|
4143
|
-
port: this.options.port,
|
|
4144
|
-
host: this.options.host,
|
|
4145
|
-
resources: Object.keys(this.options.database.resources).length
|
|
4146
|
-
};
|
|
4147
|
-
}
|
|
4148
|
-
/**
|
|
4149
|
-
* Get Hono app instance
|
|
4150
|
-
* @returns {Hono} Hono app
|
|
4151
|
-
*/
|
|
4152
|
-
getApp() {
|
|
4153
|
-
return this.app;
|
|
4154
|
-
}
|
|
4155
|
-
/**
|
|
4156
|
-
* Generate OpenAPI specification
|
|
4157
|
-
* @private
|
|
4158
|
-
* @returns {Object} OpenAPI spec
|
|
4159
|
-
*/
|
|
4160
|
-
_generateOpenAPISpec() {
|
|
4161
|
-
const { port, host, database, resources, auth, apiInfo } = this.options;
|
|
4162
|
-
return generateOpenAPISpec(database, {
|
|
4163
|
-
title: apiInfo.title,
|
|
4164
|
-
version: apiInfo.version,
|
|
4165
|
-
description: apiInfo.description,
|
|
4166
|
-
serverUrl: `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`,
|
|
4167
|
-
auth,
|
|
4168
|
-
resources
|
|
4169
|
-
});
|
|
4170
|
-
}
|
|
4171
|
-
}
|
|
4172
|
-
|
|
4173
2451
|
const PLUGIN_DEPENDENCIES = {
|
|
4174
2452
|
"postgresql-replicator": {
|
|
4175
2453
|
name: "PostgreSQL Replicator",
|
|
2454
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
|
|
4176
2455
|
dependencies: {
|
|
4177
2456
|
"pg": {
|
|
4178
2457
|
version: "^8.0.0",
|
|
4179
2458
|
description: "PostgreSQL client for Node.js",
|
|
4180
|
-
installCommand: "pnpm add pg"
|
|
2459
|
+
installCommand: "pnpm add pg",
|
|
2460
|
+
npmUrl: "https://www.npmjs.com/package/pg"
|
|
4181
2461
|
}
|
|
4182
2462
|
}
|
|
4183
2463
|
},
|
|
4184
2464
|
"bigquery-replicator": {
|
|
4185
2465
|
name: "BigQuery Replicator",
|
|
2466
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
|
|
4186
2467
|
dependencies: {
|
|
4187
2468
|
"@google-cloud/bigquery": {
|
|
4188
2469
|
version: "^7.0.0",
|
|
4189
2470
|
description: "Google Cloud BigQuery SDK",
|
|
4190
|
-
installCommand: "pnpm add @google-cloud/bigquery"
|
|
2471
|
+
installCommand: "pnpm add @google-cloud/bigquery",
|
|
2472
|
+
npmUrl: "https://www.npmjs.com/package/@google-cloud/bigquery"
|
|
4191
2473
|
}
|
|
4192
2474
|
}
|
|
4193
2475
|
},
|
|
4194
2476
|
"sqs-replicator": {
|
|
4195
2477
|
name: "SQS Replicator",
|
|
2478
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
|
|
4196
2479
|
dependencies: {
|
|
4197
2480
|
"@aws-sdk/client-sqs": {
|
|
4198
2481
|
version: "^3.0.0",
|
|
4199
2482
|
description: "AWS SDK for SQS",
|
|
4200
|
-
installCommand: "pnpm add @aws-sdk/client-sqs"
|
|
2483
|
+
installCommand: "pnpm add @aws-sdk/client-sqs",
|
|
2484
|
+
npmUrl: "https://www.npmjs.com/package/@aws-sdk/client-sqs"
|
|
4201
2485
|
}
|
|
4202
2486
|
}
|
|
4203
2487
|
},
|
|
4204
2488
|
"sqs-consumer": {
|
|
4205
2489
|
name: "SQS Queue Consumer",
|
|
2490
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md",
|
|
4206
2491
|
dependencies: {
|
|
4207
2492
|
"@aws-sdk/client-sqs": {
|
|
4208
2493
|
version: "^3.0.0",
|
|
4209
2494
|
description: "AWS SDK for SQS",
|
|
4210
|
-
installCommand: "pnpm add @aws-sdk/client-sqs"
|
|
2495
|
+
installCommand: "pnpm add @aws-sdk/client-sqs",
|
|
2496
|
+
npmUrl: "https://www.npmjs.com/package/@aws-sdk/client-sqs"
|
|
4211
2497
|
}
|
|
4212
2498
|
}
|
|
4213
2499
|
},
|
|
4214
2500
|
"rabbitmq-consumer": {
|
|
4215
2501
|
name: "RabbitMQ Queue Consumer",
|
|
2502
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md",
|
|
4216
2503
|
dependencies: {
|
|
4217
2504
|
"amqplib": {
|
|
4218
2505
|
version: "^0.10.0",
|
|
4219
2506
|
description: "AMQP 0-9-1 library for RabbitMQ",
|
|
4220
|
-
installCommand: "pnpm add amqplib"
|
|
2507
|
+
installCommand: "pnpm add amqplib",
|
|
2508
|
+
npmUrl: "https://www.npmjs.com/package/amqplib"
|
|
4221
2509
|
}
|
|
4222
2510
|
}
|
|
4223
2511
|
},
|
|
4224
2512
|
"tfstate-plugin": {
|
|
4225
|
-
name: "
|
|
2513
|
+
name: "Tfstate Plugin",
|
|
2514
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/tfstate.md",
|
|
4226
2515
|
dependencies: {
|
|
4227
2516
|
"node-cron": {
|
|
4228
2517
|
version: "^4.0.0",
|
|
4229
2518
|
description: "Cron job scheduler for auto-sync functionality",
|
|
4230
|
-
installCommand: "pnpm add node-cron"
|
|
2519
|
+
installCommand: "pnpm add node-cron",
|
|
2520
|
+
npmUrl: "https://www.npmjs.com/package/node-cron"
|
|
4231
2521
|
}
|
|
4232
2522
|
}
|
|
4233
2523
|
},
|
|
4234
2524
|
"api-plugin": {
|
|
4235
2525
|
name: "API Plugin",
|
|
2526
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/api.md",
|
|
4236
2527
|
dependencies: {
|
|
4237
2528
|
"hono": {
|
|
4238
2529
|
version: "^4.0.0",
|
|
4239
2530
|
description: "Ultra-light HTTP server framework",
|
|
4240
|
-
installCommand: "pnpm add hono"
|
|
2531
|
+
installCommand: "pnpm add hono",
|
|
2532
|
+
npmUrl: "https://www.npmjs.com/package/hono"
|
|
4241
2533
|
},
|
|
4242
2534
|
"@hono/node-server": {
|
|
4243
2535
|
version: "^1.0.0",
|
|
4244
2536
|
description: "Node.js adapter for Hono",
|
|
4245
|
-
installCommand: "pnpm add @hono/node-server"
|
|
2537
|
+
installCommand: "pnpm add @hono/node-server",
|
|
2538
|
+
npmUrl: "https://www.npmjs.com/package/@hono/node-server"
|
|
4246
2539
|
},
|
|
4247
2540
|
"@hono/swagger-ui": {
|
|
4248
2541
|
version: "^0.4.0",
|
|
4249
2542
|
description: "Swagger UI integration for Hono",
|
|
4250
|
-
installCommand: "pnpm add @hono/swagger-ui"
|
|
2543
|
+
installCommand: "pnpm add @hono/swagger-ui",
|
|
2544
|
+
npmUrl: "https://www.npmjs.com/package/@hono/swagger-ui"
|
|
4251
2545
|
}
|
|
4252
2546
|
}
|
|
4253
2547
|
}
|
|
@@ -4333,21 +2627,55 @@ async function requirePluginDependency(pluginId, options = {}) {
|
|
|
4333
2627
|
}
|
|
4334
2628
|
const valid = missing.length === 0 && incompatible.length === 0;
|
|
4335
2629
|
if (!valid && throwOnError) {
|
|
2630
|
+
const depCount = Object.keys(pluginDef.dependencies).length;
|
|
2631
|
+
const missingCount = missing.length;
|
|
2632
|
+
const incompatCount = incompatible.length;
|
|
4336
2633
|
const errorMsg = [
|
|
4337
|
-
`
|
|
4338
|
-
${pluginDef.name} - Missing dependencies detected!
|
|
4339
|
-
`,
|
|
4340
|
-
`Plugin ID: ${pluginId}`,
|
|
4341
2634
|
"",
|
|
2635
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
2636
|
+
`\u2551 \u274C ${pluginDef.name} - Missing Dependencies \u2551`,
|
|
2637
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
2638
|
+
"",
|
|
2639
|
+
`\u{1F4E6} Plugin: ${pluginId}`,
|
|
2640
|
+
`\u{1F4CA} Status: ${depCount - missingCount - incompatCount}/${depCount} dependencies satisfied`,
|
|
2641
|
+
"",
|
|
2642
|
+
"\u{1F50D} Dependency Status:",
|
|
2643
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
4342
2644
|
...messages,
|
|
4343
2645
|
"",
|
|
4344
|
-
"Quick
|
|
4345
|
-
|
|
2646
|
+
"\u{1F680} Quick Fix - Install Missing Dependencies:",
|
|
2647
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
2648
|
+
"",
|
|
2649
|
+
" Option 1: Install individually",
|
|
2650
|
+
...Object.entries(pluginDef.dependencies).filter(([pkg]) => missing.includes(pkg) || incompatible.includes(pkg)).map(([pkg, info]) => ` ${info.installCommand}`),
|
|
2651
|
+
"",
|
|
2652
|
+
" Option 2: Install all at once",
|
|
2653
|
+
` pnpm add ${Object.keys(pluginDef.dependencies).join(" ")}`,
|
|
4346
2654
|
"",
|
|
4347
|
-
"
|
|
4348
|
-
`
|
|
2655
|
+
"\u{1F4DA} Documentation:",
|
|
2656
|
+
` ${pluginDef.docsUrl}`,
|
|
2657
|
+
"",
|
|
2658
|
+
"\u{1F4A1} Troubleshooting:",
|
|
2659
|
+
" \u2022 If packages are installed but not detected, try:",
|
|
2660
|
+
" 1. Delete node_modules and reinstall: rm -rf node_modules && pnpm install",
|
|
2661
|
+
" 2. Check Node.js version: node --version (requires Node 18+)",
|
|
2662
|
+
" 3. Verify pnpm version: pnpm --version (requires pnpm 8+)",
|
|
2663
|
+
"",
|
|
2664
|
+
" \u2022 Still having issues? Check:",
|
|
2665
|
+
" - Package.json has correct dependencies listed",
|
|
2666
|
+
" - No conflicting versions in pnpm-lock.yaml",
|
|
2667
|
+
" - File permissions (especially in node_modules/)",
|
|
2668
|
+
"",
|
|
2669
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
2670
|
+
""
|
|
4349
2671
|
].join("\n");
|
|
4350
|
-
|
|
2672
|
+
const error = new Error(errorMsg);
|
|
2673
|
+
error.pluginId = pluginId;
|
|
2674
|
+
error.pluginName = pluginDef.name;
|
|
2675
|
+
error.missing = missing;
|
|
2676
|
+
error.incompatible = incompatible;
|
|
2677
|
+
error.docsUrl = pluginDef.docsUrl;
|
|
2678
|
+
throw error;
|
|
4351
2679
|
}
|
|
4352
2680
|
return { valid, missing, incompatible, messages };
|
|
4353
2681
|
}
|
|
@@ -4364,7 +2692,6 @@ class ApiPlugin extends Plugin {
|
|
|
4364
2692
|
port: options.port || 3e3,
|
|
4365
2693
|
host: options.host || "0.0.0.0",
|
|
4366
2694
|
verbose: options.verbose || false,
|
|
4367
|
-
// API Documentation (supports both new and legacy formats)
|
|
4368
2695
|
docs: {
|
|
4369
2696
|
enabled: options.docs?.enabled !== false && options.docsEnabled !== false,
|
|
4370
2697
|
// Enable by default
|
|
@@ -4681,6 +3008,11 @@ class ApiPlugin extends Plugin {
|
|
|
4681
3008
|
if (this.config.verbose) {
|
|
4682
3009
|
console.log("[API Plugin] Starting server...");
|
|
4683
3010
|
}
|
|
3011
|
+
const serverPath = "./server.js";
|
|
3012
|
+
const { ApiServer } = await import(
|
|
3013
|
+
/* @vite-ignore */
|
|
3014
|
+
serverPath
|
|
3015
|
+
);
|
|
4684
3016
|
this.server = new ApiServer({
|
|
4685
3017
|
port: this.config.port,
|
|
4686
3018
|
host: this.config.host,
|
|
@@ -5743,8 +4075,6 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
5743
4075
|
strategy: "all",
|
|
5744
4076
|
// 'all', 'any', 'priority'
|
|
5745
4077
|
concurrency: 3,
|
|
5746
|
-
requireAll: true,
|
|
5747
|
-
// For backward compatibility
|
|
5748
4078
|
...config
|
|
5749
4079
|
});
|
|
5750
4080
|
this.drivers = [];
|
|
@@ -6381,13 +4711,13 @@ class BackupPlugin extends Plugin {
|
|
|
6381
4711
|
createdAt: now.toISOString().slice(0, 10)
|
|
6382
4712
|
};
|
|
6383
4713
|
const [ok] = await tryFn(
|
|
6384
|
-
() => this.database.
|
|
4714
|
+
() => this.database.resources[this.config.backupMetadataResource].insert(metadata)
|
|
6385
4715
|
);
|
|
6386
4716
|
return metadata;
|
|
6387
4717
|
}
|
|
6388
4718
|
async _updateBackupMetadata(backupId, updates) {
|
|
6389
4719
|
const [ok] = await tryFn(
|
|
6390
|
-
() => this.database.
|
|
4720
|
+
() => this.database.resources[this.config.backupMetadataResource].update(backupId, updates)
|
|
6391
4721
|
);
|
|
6392
4722
|
}
|
|
6393
4723
|
async _createBackupManifest(type, options) {
|
|
@@ -6422,7 +4752,7 @@ class BackupPlugin extends Plugin {
|
|
|
6422
4752
|
let sinceTimestamp = null;
|
|
6423
4753
|
if (type === "incremental") {
|
|
6424
4754
|
const [lastBackupOk, , lastBackups] = await tryFn(
|
|
6425
|
-
() => this.database.
|
|
4755
|
+
() => this.database.resources[this.config.backupMetadataResource].list({
|
|
6426
4756
|
filter: {
|
|
6427
4757
|
status: "completed",
|
|
6428
4758
|
type: { $in: ["full", "incremental"] }
|
|
@@ -6730,7 +5060,7 @@ class BackupPlugin extends Plugin {
|
|
|
6730
5060
|
try {
|
|
6731
5061
|
const driverBackups = await this.driver.list(options);
|
|
6732
5062
|
const [metaOk, , metadataRecords] = await tryFn(
|
|
6733
|
-
() => this.database.
|
|
5063
|
+
() => this.database.resources[this.config.backupMetadataResource].list({
|
|
6734
5064
|
limit: options.limit || 50,
|
|
6735
5065
|
sort: { timestamp: -1 }
|
|
6736
5066
|
})
|
|
@@ -6758,14 +5088,14 @@ class BackupPlugin extends Plugin {
|
|
|
6758
5088
|
*/
|
|
6759
5089
|
async getBackupStatus(backupId) {
|
|
6760
5090
|
const [ok, , backup] = await tryFn(
|
|
6761
|
-
() => this.database.
|
|
5091
|
+
() => this.database.resources[this.config.backupMetadataResource].get(backupId)
|
|
6762
5092
|
);
|
|
6763
5093
|
return ok ? backup : null;
|
|
6764
5094
|
}
|
|
6765
5095
|
async _cleanupOldBackups() {
|
|
6766
5096
|
try {
|
|
6767
5097
|
const [listOk, , allBackups] = await tryFn(
|
|
6768
|
-
() => this.database.
|
|
5098
|
+
() => this.database.resources[this.config.backupMetadataResource].list({
|
|
6769
5099
|
filter: { status: "completed" },
|
|
6770
5100
|
sort: { timestamp: -1 }
|
|
6771
5101
|
})
|
|
@@ -6832,7 +5162,7 @@ class BackupPlugin extends Plugin {
|
|
|
6832
5162
|
for (const backup of backupsToDelete) {
|
|
6833
5163
|
try {
|
|
6834
5164
|
await this.driver.delete(backup.id, backup.driverInfo);
|
|
6835
|
-
await this.database.
|
|
5165
|
+
await this.database.resources[this.config.backupMetadataResource].delete(backup.id);
|
|
6836
5166
|
if (this.config.verbose) {
|
|
6837
5167
|
console.log(`[BackupPlugin] Deleted old backup: ${backup.id}`);
|
|
6838
5168
|
}
|
|
@@ -6868,12 +5198,6 @@ class BackupPlugin extends Plugin {
|
|
|
6868
5198
|
await this.driver.cleanup();
|
|
6869
5199
|
}
|
|
6870
5200
|
}
|
|
6871
|
-
/**
|
|
6872
|
-
* Cleanup plugin resources (alias for stop for backward compatibility)
|
|
6873
|
-
*/
|
|
6874
|
-
async cleanup() {
|
|
6875
|
-
await this.stop();
|
|
6876
|
-
}
|
|
6877
5201
|
}
|
|
6878
5202
|
|
|
6879
5203
|
class CacheError extends S3dbError {
|
|
@@ -9769,9 +8093,6 @@ async function consolidateRecord(originalId, transactionResource, targetResource
|
|
|
9769
8093
|
if (txnWithCohorts.cohortMonth && !txn.cohortMonth) {
|
|
9770
8094
|
updateData.cohortMonth = txnWithCohorts.cohortMonth;
|
|
9771
8095
|
}
|
|
9772
|
-
if (txn.value === null || txn.value === void 0) {
|
|
9773
|
-
updateData.value = 1;
|
|
9774
|
-
}
|
|
9775
8096
|
const [ok2, err2] = await tryFn(
|
|
9776
8097
|
() => transactionResource.update(txn.id, updateData)
|
|
9777
8098
|
);
|
|
@@ -11095,8 +9416,7 @@ async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
11095
9416
|
operation: "string|required",
|
|
11096
9417
|
timestamp: "string|required",
|
|
11097
9418
|
cohortDate: "string|required",
|
|
11098
|
-
cohortHour: "string|
|
|
11099
|
-
// ✅ FIX BUG #2: Changed from required to optional for migration compatibility
|
|
9419
|
+
cohortHour: "string|required",
|
|
11100
9420
|
cohortWeek: "string|optional",
|
|
11101
9421
|
cohortMonth: "string|optional",
|
|
11102
9422
|
source: "string|optional",
|
|
@@ -13430,7 +11750,7 @@ class RelationPlugin extends Plugin {
|
|
|
13430
11750
|
* @private
|
|
13431
11751
|
*/
|
|
13432
11752
|
async _setupResourceRelations(resourceName, relationsDef) {
|
|
13433
|
-
const resource = this.database.
|
|
11753
|
+
const resource = this.database.resources[resourceName];
|
|
13434
11754
|
if (!resource) {
|
|
13435
11755
|
if (this.verbose) {
|
|
13436
11756
|
console.warn(`[RelationPlugin] Resource "${resourceName}" not found, will setup when created`);
|
|
@@ -13541,7 +11861,7 @@ class RelationPlugin extends Plugin {
|
|
|
13541
11861
|
for (const record of records) {
|
|
13542
11862
|
const relatedData = record[relationName];
|
|
13543
11863
|
if (relatedData) {
|
|
13544
|
-
const relatedResource = this.database.
|
|
11864
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13545
11865
|
const relatedArray = Array.isArray(relatedData) ? relatedData : [relatedData];
|
|
13546
11866
|
if (relatedArray.length > 0) {
|
|
13547
11867
|
await this._eagerLoad(relatedArray, nestedIncludes, relatedResource);
|
|
@@ -13592,7 +11912,7 @@ class RelationPlugin extends Plugin {
|
|
|
13592
11912
|
* @private
|
|
13593
11913
|
*/
|
|
13594
11914
|
async _loadHasOne(records, relationName, config, sourceResource) {
|
|
13595
|
-
const relatedResource = this.database.
|
|
11915
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13596
11916
|
if (!relatedResource) {
|
|
13597
11917
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13598
11918
|
sourceResource: sourceResource.name,
|
|
@@ -13631,7 +11951,7 @@ class RelationPlugin extends Plugin {
|
|
|
13631
11951
|
* @private
|
|
13632
11952
|
*/
|
|
13633
11953
|
async _loadHasMany(records, relationName, config, sourceResource) {
|
|
13634
|
-
const relatedResource = this.database.
|
|
11954
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13635
11955
|
if (!relatedResource) {
|
|
13636
11956
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13637
11957
|
sourceResource: sourceResource.name,
|
|
@@ -13677,7 +11997,7 @@ class RelationPlugin extends Plugin {
|
|
|
13677
11997
|
* @private
|
|
13678
11998
|
*/
|
|
13679
11999
|
async _loadBelongsTo(records, relationName, config, sourceResource) {
|
|
13680
|
-
const relatedResource = this.database.
|
|
12000
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13681
12001
|
if (!relatedResource) {
|
|
13682
12002
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13683
12003
|
sourceResource: sourceResource.name,
|
|
@@ -13727,14 +12047,14 @@ class RelationPlugin extends Plugin {
|
|
|
13727
12047
|
* @private
|
|
13728
12048
|
*/
|
|
13729
12049
|
async _loadBelongsToMany(records, relationName, config, sourceResource) {
|
|
13730
|
-
const relatedResource = this.database.
|
|
12050
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13731
12051
|
if (!relatedResource) {
|
|
13732
12052
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13733
12053
|
sourceResource: sourceResource.name,
|
|
13734
12054
|
relation: relationName
|
|
13735
12055
|
});
|
|
13736
12056
|
}
|
|
13737
|
-
const junctionResource = this.database.
|
|
12057
|
+
const junctionResource = this.database.resources[config.through];
|
|
13738
12058
|
if (!junctionResource) {
|
|
13739
12059
|
throw new JunctionTableNotFoundError(config.through, {
|
|
13740
12060
|
sourceResource: sourceResource.name,
|
|
@@ -13918,7 +12238,7 @@ class RelationPlugin extends Plugin {
|
|
|
13918
12238
|
*/
|
|
13919
12239
|
async _cascadeDelete(record, resource, relationName, config) {
|
|
13920
12240
|
this.stats.cascadeOperations++;
|
|
13921
|
-
const relatedResource = this.database.
|
|
12241
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13922
12242
|
if (!relatedResource) {
|
|
13923
12243
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13924
12244
|
sourceResource: resource.name,
|
|
@@ -13926,7 +12246,7 @@ class RelationPlugin extends Plugin {
|
|
|
13926
12246
|
});
|
|
13927
12247
|
}
|
|
13928
12248
|
const deletedRecords = [];
|
|
13929
|
-
config.type === "belongsToMany" ? this.database.
|
|
12249
|
+
config.type === "belongsToMany" ? this.database.resources[config.through] : null;
|
|
13930
12250
|
try {
|
|
13931
12251
|
if (config.type === "hasMany") {
|
|
13932
12252
|
let relatedRecords;
|
|
@@ -13977,7 +12297,7 @@ class RelationPlugin extends Plugin {
|
|
|
13977
12297
|
await relatedResource.delete(relatedRecords[0].id);
|
|
13978
12298
|
}
|
|
13979
12299
|
} else if (config.type === "belongsToMany") {
|
|
13980
|
-
const junctionResource2 = this.database.
|
|
12300
|
+
const junctionResource2 = this.database.resources[config.through];
|
|
13981
12301
|
if (junctionResource2) {
|
|
13982
12302
|
let junctionRecords;
|
|
13983
12303
|
const partitionName = this._findPartitionByField(junctionResource2, config.foreignKey);
|
|
@@ -14045,7 +12365,7 @@ class RelationPlugin extends Plugin {
|
|
|
14045
12365
|
*/
|
|
14046
12366
|
async _cascadeUpdate(record, changes, resource, relationName, config) {
|
|
14047
12367
|
this.stats.cascadeOperations++;
|
|
14048
|
-
const relatedResource = this.database.
|
|
12368
|
+
const relatedResource = this.database.resources[config.resource];
|
|
14049
12369
|
if (!relatedResource) {
|
|
14050
12370
|
return;
|
|
14051
12371
|
}
|
|
@@ -19330,12 +17650,42 @@ ${errorDetails}`,
|
|
|
19330
17650
|
createdBy
|
|
19331
17651
|
};
|
|
19332
17652
|
this.hooks = {
|
|
17653
|
+
// Insert hooks
|
|
19333
17654
|
beforeInsert: [],
|
|
19334
17655
|
afterInsert: [],
|
|
17656
|
+
// Update hooks
|
|
19335
17657
|
beforeUpdate: [],
|
|
19336
17658
|
afterUpdate: [],
|
|
17659
|
+
// Delete hooks
|
|
19337
17660
|
beforeDelete: [],
|
|
19338
|
-
afterDelete: []
|
|
17661
|
+
afterDelete: [],
|
|
17662
|
+
// Get hooks
|
|
17663
|
+
beforeGet: [],
|
|
17664
|
+
afterGet: [],
|
|
17665
|
+
// List hooks
|
|
17666
|
+
beforeList: [],
|
|
17667
|
+
afterList: [],
|
|
17668
|
+
// Query hooks
|
|
17669
|
+
beforeQuery: [],
|
|
17670
|
+
afterQuery: [],
|
|
17671
|
+
// Patch hooks
|
|
17672
|
+
beforePatch: [],
|
|
17673
|
+
afterPatch: [],
|
|
17674
|
+
// Replace hooks
|
|
17675
|
+
beforeReplace: [],
|
|
17676
|
+
afterReplace: [],
|
|
17677
|
+
// Exists hooks
|
|
17678
|
+
beforeExists: [],
|
|
17679
|
+
afterExists: [],
|
|
17680
|
+
// Count hooks
|
|
17681
|
+
beforeCount: [],
|
|
17682
|
+
afterCount: [],
|
|
17683
|
+
// GetMany hooks
|
|
17684
|
+
beforeGetMany: [],
|
|
17685
|
+
afterGetMany: [],
|
|
17686
|
+
// DeleteMany hooks
|
|
17687
|
+
beforeDeleteMany: [],
|
|
17688
|
+
afterDeleteMany: []
|
|
19339
17689
|
};
|
|
19340
17690
|
this.attributes = attributes || {};
|
|
19341
17691
|
this.map = config.map;
|
|
@@ -19398,19 +17748,6 @@ ${errorDetails}`,
|
|
|
19398
17748
|
}
|
|
19399
17749
|
return idSize;
|
|
19400
17750
|
}
|
|
19401
|
-
/**
|
|
19402
|
-
* Get resource options (for backward compatibility with tests)
|
|
19403
|
-
*/
|
|
19404
|
-
get options() {
|
|
19405
|
-
return {
|
|
19406
|
-
timestamps: this.config.timestamps,
|
|
19407
|
-
partitions: this.config.partitions || {},
|
|
19408
|
-
cache: this.config.cache,
|
|
19409
|
-
autoDecrypt: this.config.autoDecrypt,
|
|
19410
|
-
paranoid: this.config.paranoid,
|
|
19411
|
-
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
19412
|
-
};
|
|
19413
|
-
}
|
|
19414
17751
|
export() {
|
|
19415
17752
|
const exported = this.schema.export();
|
|
19416
17753
|
exported.behavior = this.behavior;
|
|
@@ -19537,19 +17874,71 @@ ${errorDetails}`,
|
|
|
19537
17874
|
return data;
|
|
19538
17875
|
});
|
|
19539
17876
|
}
|
|
19540
|
-
|
|
17877
|
+
/**
|
|
17878
|
+
* Validate data against resource schema without saving
|
|
17879
|
+
* @param {Object} data - Data to validate
|
|
17880
|
+
* @param {Object} options - Validation options
|
|
17881
|
+
* @param {boolean} options.throwOnError - Throw error if validation fails (default: false)
|
|
17882
|
+
* @param {boolean} options.includeId - Include ID validation (default: false)
|
|
17883
|
+
* @param {boolean} options.mutateOriginal - Allow mutation of original data (default: false)
|
|
17884
|
+
* @returns {Promise<{valid: boolean, isValid: boolean, errors: Array, data: Object, original: Object}>} Validation result
|
|
17885
|
+
* @example
|
|
17886
|
+
* // Validate before insert
|
|
17887
|
+
* const result = await resource.validate({
|
|
17888
|
+
* name: 'John Doe',
|
|
17889
|
+
* email: 'invalid-email' // Will fail email validation
|
|
17890
|
+
* });
|
|
17891
|
+
*
|
|
17892
|
+
* if (!result.valid) {
|
|
17893
|
+
* console.log('Validation errors:', result.errors);
|
|
17894
|
+
* // [{ field: 'email', message: '...', ... }]
|
|
17895
|
+
* }
|
|
17896
|
+
*
|
|
17897
|
+
* // Throw on error
|
|
17898
|
+
* try {
|
|
17899
|
+
* await resource.validate({ email: 'bad' }, { throwOnError: true });
|
|
17900
|
+
* } catch (err) {
|
|
17901
|
+
* console.log('Validation failed:', err.message);
|
|
17902
|
+
* }
|
|
17903
|
+
*/
|
|
17904
|
+
async validate(data, options = {}) {
|
|
17905
|
+
const {
|
|
17906
|
+
throwOnError = false,
|
|
17907
|
+
includeId = false,
|
|
17908
|
+
mutateOriginal = false
|
|
17909
|
+
} = options;
|
|
17910
|
+
const dataToValidate = mutateOriginal ? data : cloneDeep(data);
|
|
17911
|
+
if (!includeId && dataToValidate.id) {
|
|
17912
|
+
delete dataToValidate.id;
|
|
17913
|
+
}
|
|
19541
17914
|
const result = {
|
|
19542
17915
|
original: cloneDeep(data),
|
|
19543
17916
|
isValid: false,
|
|
19544
|
-
errors: []
|
|
17917
|
+
errors: [],
|
|
17918
|
+
data: dataToValidate
|
|
19545
17919
|
};
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
17920
|
+
try {
|
|
17921
|
+
const check = await this.schema.validate(dataToValidate, { mutateOriginal });
|
|
17922
|
+
if (check === true) {
|
|
17923
|
+
result.isValid = true;
|
|
17924
|
+
} else {
|
|
17925
|
+
result.errors = Array.isArray(check) ? check : [check];
|
|
17926
|
+
result.isValid = false;
|
|
17927
|
+
if (throwOnError) {
|
|
17928
|
+
const error = new Error("Validation failed");
|
|
17929
|
+
error.validationErrors = result.errors;
|
|
17930
|
+
error.invalidData = data;
|
|
17931
|
+
throw error;
|
|
17932
|
+
}
|
|
17933
|
+
}
|
|
17934
|
+
} catch (err) {
|
|
17935
|
+
if (!throwOnError) {
|
|
17936
|
+
result.errors = [{ message: err.message, error: err }];
|
|
17937
|
+
result.isValid = false;
|
|
17938
|
+
} else {
|
|
17939
|
+
throw err;
|
|
17940
|
+
}
|
|
19551
17941
|
}
|
|
19552
|
-
result.data = data;
|
|
19553
17942
|
return result;
|
|
19554
17943
|
}
|
|
19555
17944
|
/**
|
|
@@ -19814,12 +18203,12 @@ ${errorDetails}`,
|
|
|
19814
18203
|
const exists = await this.exists(id$1);
|
|
19815
18204
|
if (exists) throw new Error(`Resource with id '${id$1}' already exists`);
|
|
19816
18205
|
this.getResourceKey(id$1 || "(auto)");
|
|
19817
|
-
if (this.
|
|
18206
|
+
if (this.config.timestamps) {
|
|
19818
18207
|
attributes.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19819
18208
|
attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19820
18209
|
}
|
|
19821
18210
|
const attributesWithDefaults = this.applyDefaults(attributes);
|
|
19822
|
-
const completeData = { id: id$1, ...attributesWithDefaults };
|
|
18211
|
+
const completeData = id$1 !== void 0 ? { id: id$1, ...attributesWithDefaults } : { ...attributesWithDefaults };
|
|
19823
18212
|
const preProcessedData = await this.executeHooks("beforeInsert", completeData);
|
|
19824
18213
|
const extraProps = Object.keys(preProcessedData).filter(
|
|
19825
18214
|
(k) => !(k in completeData) || preProcessedData[k] !== completeData[k]
|
|
@@ -19830,7 +18219,7 @@ ${errorDetails}`,
|
|
|
19830
18219
|
errors,
|
|
19831
18220
|
isValid,
|
|
19832
18221
|
data: validated
|
|
19833
|
-
} = await this.validate(preProcessedData);
|
|
18222
|
+
} = await this.validate(preProcessedData, { includeId: true });
|
|
19834
18223
|
if (!isValid) {
|
|
19835
18224
|
const errorMsg = errors && errors.length && errors[0].message ? errors[0].message : "Insert failed";
|
|
19836
18225
|
throw new InvalidResourceItem({
|
|
@@ -19934,6 +18323,7 @@ ${errorDetails}`,
|
|
|
19934
18323
|
async get(id) {
|
|
19935
18324
|
if (isObject(id)) throw new Error(`id cannot be an object`);
|
|
19936
18325
|
if (isEmpty(id)) throw new Error("id cannot be empty");
|
|
18326
|
+
await this.executeHooks("beforeGet", { id });
|
|
19937
18327
|
const key = this.getResourceKey(id);
|
|
19938
18328
|
const [ok, err, request] = await tryFn(() => this.client.getObject(key));
|
|
19939
18329
|
if (!ok) {
|
|
@@ -19982,17 +18372,67 @@ ${errorDetails}`,
|
|
|
19982
18372
|
if (objectVersion !== this.version) {
|
|
19983
18373
|
data = await this.applyVersionMapping(data, objectVersion, this.version);
|
|
19984
18374
|
}
|
|
18375
|
+
data = await this.executeHooks("afterGet", data);
|
|
19985
18376
|
this.emit("get", data);
|
|
19986
18377
|
const value = data;
|
|
19987
18378
|
return value;
|
|
19988
18379
|
}
|
|
18380
|
+
/**
|
|
18381
|
+
* Retrieve a resource object by ID, or return null if not found
|
|
18382
|
+
* @param {string} id - Resource ID
|
|
18383
|
+
* @returns {Promise<Object|null>} The resource object or null if not found
|
|
18384
|
+
* @example
|
|
18385
|
+
* const user = await resource.getOrNull('user-123');
|
|
18386
|
+
* if (user) {
|
|
18387
|
+
* console.log('Found user:', user.name);
|
|
18388
|
+
* } else {
|
|
18389
|
+
* console.log('User not found');
|
|
18390
|
+
* }
|
|
18391
|
+
*/
|
|
18392
|
+
async getOrNull(id) {
|
|
18393
|
+
const [ok, err, data] = await tryFn(() => this.get(id));
|
|
18394
|
+
if (!ok && err && (err.name === "NoSuchKey" || err.message?.includes("NoSuchKey"))) {
|
|
18395
|
+
return null;
|
|
18396
|
+
}
|
|
18397
|
+
if (!ok) {
|
|
18398
|
+
throw err;
|
|
18399
|
+
}
|
|
18400
|
+
return data;
|
|
18401
|
+
}
|
|
18402
|
+
/**
|
|
18403
|
+
* Retrieve a resource object by ID, or throw ResourceNotFoundError if not found
|
|
18404
|
+
* @param {string} id - Resource ID
|
|
18405
|
+
* @returns {Promise<Object>} The resource object
|
|
18406
|
+
* @throws {ResourceError} If resource does not exist
|
|
18407
|
+
* @example
|
|
18408
|
+
* // Throws error if user doesn't exist (no need for null check)
|
|
18409
|
+
* const user = await resource.getOrThrow('user-123');
|
|
18410
|
+
* console.log('User name:', user.name); // Safe to access
|
|
18411
|
+
*/
|
|
18412
|
+
async getOrThrow(id) {
|
|
18413
|
+
const [ok, err, data] = await tryFn(() => this.get(id));
|
|
18414
|
+
if (!ok && err && (err.name === "NoSuchKey" || err.message?.includes("NoSuchKey"))) {
|
|
18415
|
+
throw new ResourceError(`Resource '${this.name}' with id '${id}' not found`, {
|
|
18416
|
+
resourceName: this.name,
|
|
18417
|
+
operation: "getOrThrow",
|
|
18418
|
+
id,
|
|
18419
|
+
code: "RESOURCE_NOT_FOUND"
|
|
18420
|
+
});
|
|
18421
|
+
}
|
|
18422
|
+
if (!ok) {
|
|
18423
|
+
throw err;
|
|
18424
|
+
}
|
|
18425
|
+
return data;
|
|
18426
|
+
}
|
|
19989
18427
|
/**
|
|
19990
18428
|
* Check if a resource exists by ID
|
|
19991
18429
|
* @returns {Promise<boolean>} True if resource exists, false otherwise
|
|
19992
18430
|
*/
|
|
19993
18431
|
async exists(id) {
|
|
18432
|
+
await this.executeHooks("beforeExists", { id });
|
|
19994
18433
|
const key = this.getResourceKey(id);
|
|
19995
18434
|
const [ok, err] = await tryFn(() => this.client.headObject(key));
|
|
18435
|
+
await this.executeHooks("afterExists", { id, exists: ok });
|
|
19996
18436
|
return ok;
|
|
19997
18437
|
}
|
|
19998
18438
|
/**
|
|
@@ -20048,7 +18488,7 @@ ${errorDetails}`,
|
|
|
20048
18488
|
}
|
|
20049
18489
|
const preProcessedData = await this.executeHooks("beforeUpdate", cloneDeep(mergedData));
|
|
20050
18490
|
const completeData = { ...originalData, ...preProcessedData, id };
|
|
20051
|
-
const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
|
|
18491
|
+
const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
|
|
20052
18492
|
if (!isValid) {
|
|
20053
18493
|
throw new InvalidResourceItem({
|
|
20054
18494
|
bucket: this.client.config.bucket,
|
|
@@ -20221,12 +18661,17 @@ ${errorDetails}`,
|
|
|
20221
18661
|
if (!fields || typeof fields !== "object") {
|
|
20222
18662
|
throw new Error("fields must be a non-empty object");
|
|
20223
18663
|
}
|
|
18664
|
+
await this.executeHooks("beforePatch", { id, fields, options });
|
|
20224
18665
|
const behavior = this.behavior;
|
|
20225
18666
|
const hasNestedFields = Object.keys(fields).some((key) => key.includes("."));
|
|
18667
|
+
let result;
|
|
20226
18668
|
if ((behavior === "enforce-limits" || behavior === "truncate-data") && !hasNestedFields) {
|
|
20227
|
-
|
|
18669
|
+
result = await this._patchViaCopyObject(id, fields, options);
|
|
18670
|
+
} else {
|
|
18671
|
+
result = await this.update(id, fields, options);
|
|
20228
18672
|
}
|
|
20229
|
-
|
|
18673
|
+
const finalResult = await this.executeHooks("afterPatch", result);
|
|
18674
|
+
return finalResult;
|
|
20230
18675
|
}
|
|
20231
18676
|
/**
|
|
20232
18677
|
* Internal helper: Optimized patch using HeadObject + CopyObject
|
|
@@ -20326,6 +18771,7 @@ ${errorDetails}`,
|
|
|
20326
18771
|
if (!fullData || typeof fullData !== "object") {
|
|
20327
18772
|
throw new Error("fullData must be a non-empty object");
|
|
20328
18773
|
}
|
|
18774
|
+
await this.executeHooks("beforeReplace", { id, fullData, options });
|
|
20329
18775
|
const { partition, partitionValues } = options;
|
|
20330
18776
|
const dataClone = cloneDeep(fullData);
|
|
20331
18777
|
const attributesWithDefaults = this.applyDefaults(dataClone);
|
|
@@ -20340,7 +18786,7 @@ ${errorDetails}`,
|
|
|
20340
18786
|
errors,
|
|
20341
18787
|
isValid,
|
|
20342
18788
|
data: validated
|
|
20343
|
-
} = await this.validate(completeData);
|
|
18789
|
+
} = await this.validate(completeData, { includeId: true });
|
|
20344
18790
|
if (!isValid) {
|
|
20345
18791
|
const errorMsg = errors && errors.length && errors[0].message ? errors[0].message : "Replace failed";
|
|
20346
18792
|
throw new InvalidResourceItem({
|
|
@@ -20413,7 +18859,8 @@ ${errorDetails}`,
|
|
|
20413
18859
|
await this.handlePartitionReferenceUpdates({}, replacedObject);
|
|
20414
18860
|
}
|
|
20415
18861
|
}
|
|
20416
|
-
|
|
18862
|
+
const finalResult = await this.executeHooks("afterReplace", replacedObject);
|
|
18863
|
+
return finalResult;
|
|
20417
18864
|
}
|
|
20418
18865
|
/**
|
|
20419
18866
|
* Update with conditional check (If-Match ETag)
|
|
@@ -20471,7 +18918,7 @@ ${errorDetails}`,
|
|
|
20471
18918
|
}
|
|
20472
18919
|
const preProcessedData = await this.executeHooks("beforeUpdate", cloneDeep(mergedData));
|
|
20473
18920
|
const completeData = { ...originalData, ...preProcessedData, id };
|
|
20474
|
-
const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
|
|
18921
|
+
const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
|
|
20475
18922
|
if (!isValid) {
|
|
20476
18923
|
return {
|
|
20477
18924
|
success: false,
|
|
@@ -20692,6 +19139,7 @@ ${errorDetails}`,
|
|
|
20692
19139
|
* });
|
|
20693
19140
|
*/
|
|
20694
19141
|
async count({ partition = null, partitionValues = {} } = {}) {
|
|
19142
|
+
await this.executeHooks("beforeCount", { partition, partitionValues });
|
|
20695
19143
|
let prefix;
|
|
20696
19144
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
20697
19145
|
const partitionDef = this.config.partitions[partition];
|
|
@@ -20716,6 +19164,7 @@ ${errorDetails}`,
|
|
|
20716
19164
|
prefix = `resource=${this.name}/data`;
|
|
20717
19165
|
}
|
|
20718
19166
|
const count = await this.client.count({ prefix });
|
|
19167
|
+
await this.executeHooks("afterCount", { count, partition, partitionValues });
|
|
20719
19168
|
this.emit("count", count);
|
|
20720
19169
|
return count;
|
|
20721
19170
|
}
|
|
@@ -20751,6 +19200,7 @@ ${errorDetails}`,
|
|
|
20751
19200
|
* const results = await resource.deleteMany(deletedIds);
|
|
20752
19201
|
*/
|
|
20753
19202
|
async deleteMany(ids) {
|
|
19203
|
+
await this.executeHooks("beforeDeleteMany", { ids });
|
|
20754
19204
|
const packages = chunk(
|
|
20755
19205
|
ids.map((id) => this.getResourceKey(id)),
|
|
20756
19206
|
1e3
|
|
@@ -20772,6 +19222,7 @@ ${errorDetails}`,
|
|
|
20772
19222
|
});
|
|
20773
19223
|
return response;
|
|
20774
19224
|
});
|
|
19225
|
+
await this.executeHooks("afterDeleteMany", { ids, results });
|
|
20775
19226
|
this.emit("deleteMany", ids.length);
|
|
20776
19227
|
return results;
|
|
20777
19228
|
}
|
|
@@ -20893,6 +19344,7 @@ ${errorDetails}`,
|
|
|
20893
19344
|
* });
|
|
20894
19345
|
*/
|
|
20895
19346
|
async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
|
|
19347
|
+
await this.executeHooks("beforeList", { partition, partitionValues, limit, offset });
|
|
20896
19348
|
const [ok, err, result] = await tryFn(async () => {
|
|
20897
19349
|
if (!partition) {
|
|
20898
19350
|
return await this.listMain({ limit, offset });
|
|
@@ -20902,7 +19354,8 @@ ${errorDetails}`,
|
|
|
20902
19354
|
if (!ok) {
|
|
20903
19355
|
return this.handleListError(err, { partition, partitionValues });
|
|
20904
19356
|
}
|
|
20905
|
-
|
|
19357
|
+
const finalResult = await this.executeHooks("afterList", result);
|
|
19358
|
+
return finalResult;
|
|
20906
19359
|
}
|
|
20907
19360
|
async listMain({ limit, offset = 0 }) {
|
|
20908
19361
|
const [ok, err, ids] = await tryFn(() => this.listIds({ limit, offset }));
|
|
@@ -21045,6 +19498,7 @@ ${errorDetails}`,
|
|
|
21045
19498
|
* const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
|
|
21046
19499
|
*/
|
|
21047
19500
|
async getMany(ids) {
|
|
19501
|
+
await this.executeHooks("beforeGetMany", { ids });
|
|
21048
19502
|
const { results, errors } = await PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
|
|
21049
19503
|
this.emit("error", error, content);
|
|
21050
19504
|
this.observers.map((x) => x.emit("error", this.name, error, content));
|
|
@@ -21065,8 +19519,9 @@ ${errorDetails}`,
|
|
|
21065
19519
|
}
|
|
21066
19520
|
throw err;
|
|
21067
19521
|
});
|
|
19522
|
+
const finalResults = await this.executeHooks("afterGetMany", results);
|
|
21068
19523
|
this.emit("getMany", ids.length);
|
|
21069
|
-
return
|
|
19524
|
+
return finalResults;
|
|
21070
19525
|
}
|
|
21071
19526
|
/**
|
|
21072
19527
|
* Get all resources (equivalent to list() without pagination)
|
|
@@ -21313,21 +19768,6 @@ ${errorDetails}`,
|
|
|
21313
19768
|
* @returns {Object} Schema object for the version
|
|
21314
19769
|
*/
|
|
21315
19770
|
async getSchemaForVersion(version) {
|
|
21316
|
-
if (version === this.version) {
|
|
21317
|
-
return this.schema;
|
|
21318
|
-
}
|
|
21319
|
-
const [ok, err, compatibleSchema] = await tryFn(() => Promise.resolve(new Schema({
|
|
21320
|
-
name: this.name,
|
|
21321
|
-
attributes: this.attributes,
|
|
21322
|
-
passphrase: this.passphrase,
|
|
21323
|
-
version,
|
|
21324
|
-
options: {
|
|
21325
|
-
...this.config,
|
|
21326
|
-
autoDecrypt: true,
|
|
21327
|
-
autoEncrypt: true
|
|
21328
|
-
}
|
|
21329
|
-
})));
|
|
21330
|
-
if (ok) return compatibleSchema;
|
|
21331
19771
|
return this.schema;
|
|
21332
19772
|
}
|
|
21333
19773
|
/**
|
|
@@ -21423,6 +19863,7 @@ ${errorDetails}`,
|
|
|
21423
19863
|
* );
|
|
21424
19864
|
*/
|
|
21425
19865
|
async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
|
|
19866
|
+
await this.executeHooks("beforeQuery", { filter, limit, offset, partition, partitionValues });
|
|
21426
19867
|
if (Object.keys(filter).length === 0) {
|
|
21427
19868
|
return await this.list({ partition, partitionValues, limit, offset });
|
|
21428
19869
|
}
|
|
@@ -21450,7 +19891,8 @@ ${errorDetails}`,
|
|
|
21450
19891
|
break;
|
|
21451
19892
|
}
|
|
21452
19893
|
}
|
|
21453
|
-
|
|
19894
|
+
const finalResults = results.slice(0, limit);
|
|
19895
|
+
return await this.executeHooks("afterQuery", finalResults);
|
|
21454
19896
|
}
|
|
21455
19897
|
/**
|
|
21456
19898
|
* Handle partition reference updates with change detection
|
|
@@ -21530,7 +19972,7 @@ ${errorDetails}`,
|
|
|
21530
19972
|
}
|
|
21531
19973
|
}
|
|
21532
19974
|
/**
|
|
21533
|
-
* Update partition objects to keep them in sync
|
|
19975
|
+
* Update partition objects to keep them in sync
|
|
21534
19976
|
* @param {Object} data - Updated object data
|
|
21535
19977
|
*/
|
|
21536
19978
|
async updatePartitionReferences(data) {
|
|
@@ -21916,7 +20358,32 @@ function validateResourceConfig(config) {
|
|
|
21916
20358
|
if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
|
|
21917
20359
|
errors.push("Resource 'hooks' must be an object");
|
|
21918
20360
|
} else {
|
|
21919
|
-
const validHookEvents = [
|
|
20361
|
+
const validHookEvents = [
|
|
20362
|
+
"beforeInsert",
|
|
20363
|
+
"afterInsert",
|
|
20364
|
+
"beforeUpdate",
|
|
20365
|
+
"afterUpdate",
|
|
20366
|
+
"beforeDelete",
|
|
20367
|
+
"afterDelete",
|
|
20368
|
+
"beforeGet",
|
|
20369
|
+
"afterGet",
|
|
20370
|
+
"beforeList",
|
|
20371
|
+
"afterList",
|
|
20372
|
+
"beforeQuery",
|
|
20373
|
+
"afterQuery",
|
|
20374
|
+
"beforeExists",
|
|
20375
|
+
"afterExists",
|
|
20376
|
+
"beforeCount",
|
|
20377
|
+
"afterCount",
|
|
20378
|
+
"beforePatch",
|
|
20379
|
+
"afterPatch",
|
|
20380
|
+
"beforeReplace",
|
|
20381
|
+
"afterReplace",
|
|
20382
|
+
"beforeGetMany",
|
|
20383
|
+
"afterGetMany",
|
|
20384
|
+
"beforeDeleteMany",
|
|
20385
|
+
"afterDeleteMany"
|
|
20386
|
+
];
|
|
21920
20387
|
for (const [event, hooksArr] of Object.entries(config.hooks)) {
|
|
21921
20388
|
if (!validHookEvents.includes(event)) {
|
|
21922
20389
|
errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(", ")}`);
|
|
@@ -21964,17 +20431,35 @@ class Database extends EventEmitter {
|
|
|
21964
20431
|
this.id = idGenerator(7);
|
|
21965
20432
|
this.version = "1";
|
|
21966
20433
|
this.s3dbVersion = (() => {
|
|
21967
|
-
const [ok, err, version] = tryFn(() => true ? "12.1
|
|
20434
|
+
const [ok, err, version] = tryFn(() => true ? "12.2.1" : "latest");
|
|
21968
20435
|
return ok ? version : "latest";
|
|
21969
20436
|
})();
|
|
21970
|
-
this.
|
|
20437
|
+
this._resourcesMap = {};
|
|
20438
|
+
this.resources = new Proxy(this._resourcesMap, {
|
|
20439
|
+
get: (target, prop) => {
|
|
20440
|
+
if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
|
|
20441
|
+
return target[prop];
|
|
20442
|
+
}
|
|
20443
|
+
if (target[prop]) {
|
|
20444
|
+
return target[prop];
|
|
20445
|
+
}
|
|
20446
|
+
return void 0;
|
|
20447
|
+
},
|
|
20448
|
+
// Support Object.keys(), Object.entries(), etc.
|
|
20449
|
+
ownKeys: (target) => {
|
|
20450
|
+
return Object.keys(target);
|
|
20451
|
+
},
|
|
20452
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
20453
|
+
return Object.getOwnPropertyDescriptor(target, prop);
|
|
20454
|
+
}
|
|
20455
|
+
});
|
|
21971
20456
|
this.savedMetadata = null;
|
|
21972
20457
|
this.options = options;
|
|
21973
20458
|
this.verbose = options.verbose || false;
|
|
21974
20459
|
this.parallelism = parseInt(options.parallelism + "") || 10;
|
|
21975
|
-
this.plugins = options.plugins || [];
|
|
21976
|
-
this.pluginRegistry = {};
|
|
21977
20460
|
this.pluginList = options.plugins || [];
|
|
20461
|
+
this.pluginRegistry = {};
|
|
20462
|
+
this.plugins = this.pluginRegistry;
|
|
21978
20463
|
this.cache = options.cache;
|
|
21979
20464
|
this.passphrase = options.passphrase || "secret";
|
|
21980
20465
|
this.versioningEnabled = options.versioningEnabled || false;
|
|
@@ -22080,7 +20565,7 @@ class Database extends EventEmitter {
|
|
|
22080
20565
|
} else {
|
|
22081
20566
|
restoredIdSize = versionData.idSize || 22;
|
|
22082
20567
|
}
|
|
22083
|
-
this.
|
|
20568
|
+
this._resourcesMap[name] = new Resource({
|
|
22084
20569
|
name,
|
|
22085
20570
|
client: this.client,
|
|
22086
20571
|
database: this,
|
|
@@ -22149,7 +20634,7 @@ class Database extends EventEmitter {
|
|
|
22149
20634
|
}
|
|
22150
20635
|
}
|
|
22151
20636
|
for (const [name, savedResource] of Object.entries(savedMetadata.resources || {})) {
|
|
22152
|
-
if (!this.
|
|
20637
|
+
if (!this._resourcesMap[name]) {
|
|
22153
20638
|
const currentVersion = savedResource.currentVersion || "v1";
|
|
22154
20639
|
const versionData = savedResource.versions?.[currentVersion];
|
|
22155
20640
|
changes.push({
|
|
@@ -22661,7 +21146,7 @@ class Database extends EventEmitter {
|
|
|
22661
21146
|
* @returns {boolean} True if resource exists, false otherwise
|
|
22662
21147
|
*/
|
|
22663
21148
|
resourceExists(name) {
|
|
22664
|
-
return !!this.
|
|
21149
|
+
return !!this._resourcesMap[name];
|
|
22665
21150
|
}
|
|
22666
21151
|
/**
|
|
22667
21152
|
* Check if a resource exists with the same definition hash
|
|
@@ -22669,14 +21154,13 @@ class Database extends EventEmitter {
|
|
|
22669
21154
|
* @param {string} config.name - Resource name
|
|
22670
21155
|
* @param {Object} config.attributes - Resource attributes
|
|
22671
21156
|
* @param {string} [config.behavior] - Resource behavior
|
|
22672
|
-
* @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
|
|
22673
21157
|
* @returns {Object} Result with exists and hash information
|
|
22674
21158
|
*/
|
|
22675
|
-
resourceExistsWithSameHash({ name, attributes, behavior = "user-managed", partitions = {}
|
|
22676
|
-
if (!this.
|
|
21159
|
+
resourceExistsWithSameHash({ name, attributes, behavior = "user-managed", partitions = {} }) {
|
|
21160
|
+
if (!this._resourcesMap[name]) {
|
|
22677
21161
|
return { exists: false, sameHash: false, hash: null };
|
|
22678
21162
|
}
|
|
22679
|
-
const existingResource = this.
|
|
21163
|
+
const existingResource = this._resourcesMap[name];
|
|
22680
21164
|
const existingHash = this.generateDefinitionHash(existingResource.export());
|
|
22681
21165
|
const mockResource = new Resource({
|
|
22682
21166
|
name,
|
|
@@ -22686,8 +21170,7 @@ class Database extends EventEmitter {
|
|
|
22686
21170
|
client: this.client,
|
|
22687
21171
|
version: existingResource.version,
|
|
22688
21172
|
passphrase: this.passphrase,
|
|
22689
|
-
versioningEnabled: this.versioningEnabled
|
|
22690
|
-
...options
|
|
21173
|
+
versioningEnabled: this.versioningEnabled
|
|
22691
21174
|
});
|
|
22692
21175
|
const newHash = this.generateDefinitionHash(mockResource.export());
|
|
22693
21176
|
return {
|
|
@@ -22715,12 +21198,49 @@ class Database extends EventEmitter {
|
|
|
22715
21198
|
* @param {string} [config.createdBy='user'] - Who created this resource ('user', 'plugin', or plugin name)
|
|
22716
21199
|
* @returns {Promise<Resource>} The created or updated resource
|
|
22717
21200
|
*/
|
|
22718
|
-
|
|
22719
|
-
|
|
22720
|
-
|
|
21201
|
+
/**
|
|
21202
|
+
* Normalize partitions config from array or object format
|
|
21203
|
+
* @param {Array|Object} partitions - Partitions config
|
|
21204
|
+
* @param {Object} attributes - Resource attributes
|
|
21205
|
+
* @returns {Object} Normalized partitions object
|
|
21206
|
+
* @private
|
|
21207
|
+
*/
|
|
21208
|
+
_normalizePartitions(partitions, attributes) {
|
|
21209
|
+
if (!Array.isArray(partitions)) {
|
|
21210
|
+
return partitions || {};
|
|
21211
|
+
}
|
|
21212
|
+
const normalized = {};
|
|
21213
|
+
for (const fieldName of partitions) {
|
|
21214
|
+
if (typeof fieldName !== "string") {
|
|
21215
|
+
throw new Error(`Partition field must be a string, got ${typeof fieldName}`);
|
|
21216
|
+
}
|
|
21217
|
+
if (!attributes[fieldName]) {
|
|
21218
|
+
throw new Error(`Partition field '${fieldName}' not found in attributes`);
|
|
21219
|
+
}
|
|
21220
|
+
const partitionName = `by${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
|
|
21221
|
+
const fieldDef = attributes[fieldName];
|
|
21222
|
+
let fieldType = "string";
|
|
21223
|
+
if (typeof fieldDef === "string") {
|
|
21224
|
+
fieldType = fieldDef.split("|")[0].trim();
|
|
21225
|
+
} else if (typeof fieldDef === "object" && fieldDef.type) {
|
|
21226
|
+
fieldType = fieldDef.type;
|
|
21227
|
+
}
|
|
21228
|
+
normalized[partitionName] = {
|
|
21229
|
+
fields: {
|
|
21230
|
+
[fieldName]: fieldType
|
|
21231
|
+
}
|
|
21232
|
+
};
|
|
21233
|
+
}
|
|
21234
|
+
return normalized;
|
|
21235
|
+
}
|
|
21236
|
+
async createResource({ name, attributes, behavior = "user-managed", hooks, middlewares, ...config }) {
|
|
21237
|
+
const normalizedPartitions = this._normalizePartitions(config.partitions, attributes);
|
|
21238
|
+
if (this._resourcesMap[name]) {
|
|
21239
|
+
const existingResource = this._resourcesMap[name];
|
|
22721
21240
|
Object.assign(existingResource.config, {
|
|
22722
21241
|
cache: this.cache,
|
|
22723
|
-
...config
|
|
21242
|
+
...config,
|
|
21243
|
+
partitions: normalizedPartitions
|
|
22724
21244
|
});
|
|
22725
21245
|
if (behavior) {
|
|
22726
21246
|
existingResource.behavior = behavior;
|
|
@@ -22738,6 +21258,9 @@ class Database extends EventEmitter {
|
|
|
22738
21258
|
}
|
|
22739
21259
|
}
|
|
22740
21260
|
}
|
|
21261
|
+
if (middlewares) {
|
|
21262
|
+
this._applyMiddlewares(existingResource, middlewares);
|
|
21263
|
+
}
|
|
22741
21264
|
const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
|
|
22742
21265
|
const existingMetadata2 = this.savedMetadata?.resources?.[name];
|
|
22743
21266
|
const currentVersion = existingMetadata2?.currentVersion || "v1";
|
|
@@ -22761,7 +21284,7 @@ class Database extends EventEmitter {
|
|
|
22761
21284
|
observers: [this],
|
|
22762
21285
|
cache: config.cache !== void 0 ? config.cache : this.cache,
|
|
22763
21286
|
timestamps: config.timestamps !== void 0 ? config.timestamps : false,
|
|
22764
|
-
partitions:
|
|
21287
|
+
partitions: normalizedPartitions,
|
|
22765
21288
|
paranoid: config.paranoid !== void 0 ? config.paranoid : true,
|
|
22766
21289
|
allNestedObjectsOptional: config.allNestedObjectsOptional !== void 0 ? config.allNestedObjectsOptional : true,
|
|
22767
21290
|
autoDecrypt: config.autoDecrypt !== void 0 ? config.autoDecrypt : true,
|
|
@@ -22777,16 +21300,96 @@ class Database extends EventEmitter {
|
|
|
22777
21300
|
createdBy: config.createdBy || "user"
|
|
22778
21301
|
});
|
|
22779
21302
|
resource.database = this;
|
|
22780
|
-
this.
|
|
21303
|
+
this._resourcesMap[name] = resource;
|
|
21304
|
+
if (middlewares) {
|
|
21305
|
+
this._applyMiddlewares(resource, middlewares);
|
|
21306
|
+
}
|
|
22781
21307
|
await this.uploadMetadataFile();
|
|
22782
21308
|
this.emit("s3db.resourceCreated", name);
|
|
22783
21309
|
return resource;
|
|
22784
21310
|
}
|
|
22785
|
-
|
|
22786
|
-
|
|
22787
|
-
|
|
21311
|
+
/**
|
|
21312
|
+
* Apply middlewares to a resource
|
|
21313
|
+
* @param {Resource} resource - Resource instance
|
|
21314
|
+
* @param {Array|Object} middlewares - Middlewares config
|
|
21315
|
+
* @private
|
|
21316
|
+
*/
|
|
21317
|
+
_applyMiddlewares(resource, middlewares) {
|
|
21318
|
+
if (Array.isArray(middlewares)) {
|
|
21319
|
+
const methods = resource._middlewareMethods || [
|
|
21320
|
+
"get",
|
|
21321
|
+
"list",
|
|
21322
|
+
"listIds",
|
|
21323
|
+
"getAll",
|
|
21324
|
+
"count",
|
|
21325
|
+
"page",
|
|
21326
|
+
"insert",
|
|
21327
|
+
"update",
|
|
21328
|
+
"delete",
|
|
21329
|
+
"deleteMany",
|
|
21330
|
+
"exists",
|
|
21331
|
+
"getMany",
|
|
21332
|
+
"content",
|
|
21333
|
+
"hasContent",
|
|
21334
|
+
"query",
|
|
21335
|
+
"getFromPartition",
|
|
21336
|
+
"setContent",
|
|
21337
|
+
"deleteContent",
|
|
21338
|
+
"replace",
|
|
21339
|
+
"patch"
|
|
21340
|
+
];
|
|
21341
|
+
for (const method of methods) {
|
|
21342
|
+
for (const middleware of middlewares) {
|
|
21343
|
+
if (typeof middleware === "function") {
|
|
21344
|
+
resource.useMiddleware(method, middleware);
|
|
21345
|
+
}
|
|
21346
|
+
}
|
|
21347
|
+
}
|
|
21348
|
+
return;
|
|
21349
|
+
}
|
|
21350
|
+
if (typeof middlewares === "object" && middlewares !== null) {
|
|
21351
|
+
for (const [method, fns] of Object.entries(middlewares)) {
|
|
21352
|
+
if (method === "*") {
|
|
21353
|
+
const methods = resource._middlewareMethods || [
|
|
21354
|
+
"get",
|
|
21355
|
+
"list",
|
|
21356
|
+
"listIds",
|
|
21357
|
+
"getAll",
|
|
21358
|
+
"count",
|
|
21359
|
+
"page",
|
|
21360
|
+
"insert",
|
|
21361
|
+
"update",
|
|
21362
|
+
"delete",
|
|
21363
|
+
"deleteMany",
|
|
21364
|
+
"exists",
|
|
21365
|
+
"getMany",
|
|
21366
|
+
"content",
|
|
21367
|
+
"hasContent",
|
|
21368
|
+
"query",
|
|
21369
|
+
"getFromPartition",
|
|
21370
|
+
"setContent",
|
|
21371
|
+
"deleteContent",
|
|
21372
|
+
"replace",
|
|
21373
|
+
"patch"
|
|
21374
|
+
];
|
|
21375
|
+
const middlewareArray = Array.isArray(fns) ? fns : [fns];
|
|
21376
|
+
for (const targetMethod of methods) {
|
|
21377
|
+
for (const middleware of middlewareArray) {
|
|
21378
|
+
if (typeof middleware === "function") {
|
|
21379
|
+
resource.useMiddleware(targetMethod, middleware);
|
|
21380
|
+
}
|
|
21381
|
+
}
|
|
21382
|
+
}
|
|
21383
|
+
} else {
|
|
21384
|
+
const middlewareArray = Array.isArray(fns) ? fns : [fns];
|
|
21385
|
+
for (const middleware of middlewareArray) {
|
|
21386
|
+
if (typeof middleware === "function") {
|
|
21387
|
+
resource.useMiddleware(method, middleware);
|
|
21388
|
+
}
|
|
21389
|
+
}
|
|
21390
|
+
}
|
|
21391
|
+
}
|
|
22788
21392
|
}
|
|
22789
|
-
return this.resources[name];
|
|
22790
21393
|
}
|
|
22791
21394
|
/**
|
|
22792
21395
|
* List all resource names
|
|
@@ -22801,14 +21404,14 @@ class Database extends EventEmitter {
|
|
|
22801
21404
|
* @returns {Resource} Resource instance
|
|
22802
21405
|
*/
|
|
22803
21406
|
async getResource(name) {
|
|
22804
|
-
if (!this.
|
|
21407
|
+
if (!this._resourcesMap[name]) {
|
|
22805
21408
|
throw new ResourceNotFound({
|
|
22806
21409
|
bucket: this.client.config.bucket,
|
|
22807
21410
|
resourceName: name,
|
|
22808
21411
|
id: name
|
|
22809
21412
|
});
|
|
22810
21413
|
}
|
|
22811
|
-
return this.
|
|
21414
|
+
return this._resourcesMap[name];
|
|
22812
21415
|
}
|
|
22813
21416
|
/**
|
|
22814
21417
|
* Get database configuration
|
|
@@ -22861,7 +21464,7 @@ class Database extends EventEmitter {
|
|
|
22861
21464
|
}
|
|
22862
21465
|
});
|
|
22863
21466
|
}
|
|
22864
|
-
Object.keys(this.resources).forEach((k) => delete this.
|
|
21467
|
+
Object.keys(this.resources).forEach((k) => delete this._resourcesMap[k]);
|
|
22865
21468
|
}
|
|
22866
21469
|
if (this.client && typeof this.client.removeAllListeners === "function") {
|
|
22867
21470
|
this.client.removeAllListeners();
|
|
@@ -24605,14 +23208,6 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24605
23208
|
}
|
|
24606
23209
|
async start() {
|
|
24607
23210
|
}
|
|
24608
|
-
async stop() {
|
|
24609
|
-
for (const replicator of this.replicators || []) {
|
|
24610
|
-
if (replicator && typeof replicator.cleanup === "function") {
|
|
24611
|
-
await replicator.cleanup();
|
|
24612
|
-
}
|
|
24613
|
-
}
|
|
24614
|
-
this.removeDatabaseHooks();
|
|
24615
|
-
}
|
|
24616
23211
|
installDatabaseHooks() {
|
|
24617
23212
|
this._afterCreateResourceHook = (resource) => {
|
|
24618
23213
|
if (resource.name !== (this.config.replicatorLogResource || "plg_replicator_logs")) {
|
|
@@ -24942,20 +23537,20 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24942
23537
|
}
|
|
24943
23538
|
this.emit("replicator.sync.completed", { replicatorId, stats: this.stats });
|
|
24944
23539
|
}
|
|
24945
|
-
async
|
|
23540
|
+
async stop() {
|
|
24946
23541
|
const [ok, error] = await tryFn(async () => {
|
|
24947
23542
|
if (this.replicators && this.replicators.length > 0) {
|
|
24948
23543
|
const cleanupPromises = this.replicators.map(async (replicator) => {
|
|
24949
23544
|
const [replicatorOk, replicatorError] = await tryFn(async () => {
|
|
24950
|
-
if (replicator && typeof replicator.
|
|
24951
|
-
await replicator.
|
|
23545
|
+
if (replicator && typeof replicator.stop === "function") {
|
|
23546
|
+
await replicator.stop();
|
|
24952
23547
|
}
|
|
24953
23548
|
});
|
|
24954
23549
|
if (!replicatorOk) {
|
|
24955
23550
|
if (this.config.verbose) {
|
|
24956
|
-
console.warn(`[ReplicatorPlugin] Failed to
|
|
23551
|
+
console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
|
|
24957
23552
|
}
|
|
24958
|
-
this.emit("
|
|
23553
|
+
this.emit("replicator_stop_error", {
|
|
24959
23554
|
replicator: replicator.name || replicator.id || "unknown",
|
|
24960
23555
|
driver: replicator.driver || "unknown",
|
|
24961
23556
|
error: replicatorError.message
|
|
@@ -24964,6 +23559,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24964
23559
|
});
|
|
24965
23560
|
await Promise.allSettled(cleanupPromises);
|
|
24966
23561
|
}
|
|
23562
|
+
this.removeDatabaseHooks();
|
|
24967
23563
|
if (this.database && this.database.resources) {
|
|
24968
23564
|
for (const resourceName of this.eventListenersInstalled) {
|
|
24969
23565
|
const resource = this.database.resources[resourceName];
|
|
@@ -24983,9 +23579,9 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24983
23579
|
});
|
|
24984
23580
|
if (!ok) {
|
|
24985
23581
|
if (this.config.verbose) {
|
|
24986
|
-
console.warn(`[ReplicatorPlugin] Failed to
|
|
23582
|
+
console.warn(`[ReplicatorPlugin] Failed to stop plugin: ${error.message}`);
|
|
24987
23583
|
}
|
|
24988
|
-
this.emit("
|
|
23584
|
+
this.emit("replicator_plugin_stop_error", {
|
|
24989
23585
|
error: error.message
|
|
24990
23586
|
});
|
|
24991
23587
|
}
|
|
@@ -25805,7 +24401,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
25805
24401
|
}
|
|
25806
24402
|
async _persistJobExecution(jobName, executionId, startTime, endTime, duration, status, result, error, retryCount) {
|
|
25807
24403
|
const [ok, err] = await tryFn(
|
|
25808
|
-
() => this.database.
|
|
24404
|
+
() => this.database.resources[this.config.jobHistoryResource].insert({
|
|
25809
24405
|
id: executionId,
|
|
25810
24406
|
jobName,
|
|
25811
24407
|
status,
|
|
@@ -25946,7 +24542,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
25946
24542
|
queryParams.status = status;
|
|
25947
24543
|
}
|
|
25948
24544
|
const [ok, err, history] = await tryFn(
|
|
25949
|
-
() => this.database.
|
|
24545
|
+
() => this.database.resources[this.config.jobHistoryResource].query(queryParams)
|
|
25950
24546
|
);
|
|
25951
24547
|
if (!ok) {
|
|
25952
24548
|
if (this.config.verbose) {
|
|
@@ -26085,9 +24681,6 @@ class SchedulerPlugin extends Plugin {
|
|
|
26085
24681
|
if (this._isTestEnvironment()) {
|
|
26086
24682
|
this.activeJobs.clear();
|
|
26087
24683
|
}
|
|
26088
|
-
}
|
|
26089
|
-
async cleanup() {
|
|
26090
|
-
await this.stop();
|
|
26091
24684
|
this.jobs.clear();
|
|
26092
24685
|
this.statistics.clear();
|
|
26093
24686
|
this.activeJobs.clear();
|
|
@@ -26336,7 +24929,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26336
24929
|
let lastLogErr;
|
|
26337
24930
|
for (let attempt = 0; attempt < this.config.retryAttempts; attempt++) {
|
|
26338
24931
|
const [ok, err] = await tryFn(
|
|
26339
|
-
() => this.database.
|
|
24932
|
+
() => this.database.resources[this.config.transitionLogResource].insert({
|
|
26340
24933
|
id: transitionId,
|
|
26341
24934
|
machineId,
|
|
26342
24935
|
entityId,
|
|
@@ -26372,11 +24965,11 @@ class StateMachinePlugin extends Plugin {
|
|
|
26372
24965
|
updatedAt: now
|
|
26373
24966
|
};
|
|
26374
24967
|
const [updateOk] = await tryFn(
|
|
26375
|
-
() => this.database.
|
|
24968
|
+
() => this.database.resources[this.config.stateResource].update(stateId, stateData)
|
|
26376
24969
|
);
|
|
26377
24970
|
if (!updateOk) {
|
|
26378
24971
|
const [insertOk, insertErr] = await tryFn(
|
|
26379
|
-
() => this.database.
|
|
24972
|
+
() => this.database.resources[this.config.stateResource].insert({ id: stateId, ...stateData })
|
|
26380
24973
|
);
|
|
26381
24974
|
if (!insertOk && this.config.verbose) {
|
|
26382
24975
|
console.warn(`[StateMachinePlugin] Failed to upsert state:`, insertErr.message);
|
|
@@ -26439,7 +25032,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26439
25032
|
if (this.config.persistTransitions) {
|
|
26440
25033
|
const stateId = `${machineId}_${entityId}`;
|
|
26441
25034
|
const [ok, err, stateRecord] = await tryFn(
|
|
26442
|
-
() => this.database.
|
|
25035
|
+
() => this.database.resources[this.config.stateResource].get(stateId)
|
|
26443
25036
|
);
|
|
26444
25037
|
if (ok && stateRecord) {
|
|
26445
25038
|
machine.currentStates.set(entityId, stateRecord.currentState);
|
|
@@ -26482,7 +25075,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26482
25075
|
}
|
|
26483
25076
|
const { limit = 50, offset = 0 } = options;
|
|
26484
25077
|
const [ok, err, transitions] = await tryFn(
|
|
26485
|
-
() => this.database.
|
|
25078
|
+
() => this.database.resources[this.config.transitionLogResource].query({
|
|
26486
25079
|
machineId,
|
|
26487
25080
|
entityId
|
|
26488
25081
|
}, {
|
|
@@ -26524,7 +25117,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26524
25117
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26525
25118
|
const stateId = `${machineId}_${entityId}`;
|
|
26526
25119
|
const [ok, err] = await tryFn(
|
|
26527
|
-
() => this.database.
|
|
25120
|
+
() => this.database.resources[this.config.stateResource].insert({
|
|
26528
25121
|
id: stateId,
|
|
26529
25122
|
machineId,
|
|
26530
25123
|
entityId,
|
|
@@ -26613,9 +25206,6 @@ class StateMachinePlugin extends Plugin {
|
|
|
26613
25206
|
}
|
|
26614
25207
|
async stop() {
|
|
26615
25208
|
this.machines.clear();
|
|
26616
|
-
}
|
|
26617
|
-
async cleanup() {
|
|
26618
|
-
await this.stop();
|
|
26619
25209
|
this.removeAllListeners();
|
|
26620
25210
|
}
|
|
26621
25211
|
}
|
|
@@ -26630,7 +25220,7 @@ class TfStateError extends Error {
|
|
|
26630
25220
|
}
|
|
26631
25221
|
class InvalidStateFileError extends TfStateError {
|
|
26632
25222
|
constructor(filePath, reason, context = {}) {
|
|
26633
|
-
super(`Invalid
|
|
25223
|
+
super(`Invalid Tfstate file "${filePath}": ${reason}`, context);
|
|
26634
25224
|
this.name = "InvalidStateFileError";
|
|
26635
25225
|
this.filePath = filePath;
|
|
26636
25226
|
this.reason = reason;
|
|
@@ -26639,7 +25229,7 @@ class InvalidStateFileError extends TfStateError {
|
|
|
26639
25229
|
class UnsupportedStateVersionError extends TfStateError {
|
|
26640
25230
|
constructor(version, supportedVersions, context = {}) {
|
|
26641
25231
|
super(
|
|
26642
|
-
`
|
|
25232
|
+
`Tfstate version ${version} is not supported. Supported versions: ${supportedVersions.join(", ")}`,
|
|
26643
25233
|
context
|
|
26644
25234
|
);
|
|
26645
25235
|
this.name = "UnsupportedStateVersionError";
|
|
@@ -26649,7 +25239,7 @@ class UnsupportedStateVersionError extends TfStateError {
|
|
|
26649
25239
|
}
|
|
26650
25240
|
class StateFileNotFoundError extends TfStateError {
|
|
26651
25241
|
constructor(filePath, context = {}) {
|
|
26652
|
-
super(`
|
|
25242
|
+
super(`Tfstate file not found: ${filePath}`, context);
|
|
26653
25243
|
this.name = "StateFileNotFoundError";
|
|
26654
25244
|
this.filePath = filePath;
|
|
26655
25245
|
}
|
|
@@ -34872,42 +33462,23 @@ class FilesystemTfStateDriver extends TfStateDriver {
|
|
|
34872
33462
|
class TfStatePlugin extends Plugin {
|
|
34873
33463
|
constructor(config = {}) {
|
|
34874
33464
|
super(config);
|
|
34875
|
-
|
|
34876
|
-
|
|
34877
|
-
|
|
34878
|
-
|
|
34879
|
-
|
|
34880
|
-
|
|
34881
|
-
|
|
34882
|
-
|
|
34883
|
-
|
|
34884
|
-
|
|
34885
|
-
|
|
34886
|
-
|
|
34887
|
-
|
|
34888
|
-
|
|
34889
|
-
|
|
34890
|
-
|
|
34891
|
-
|
|
34892
|
-
this.filters = config.filters || {};
|
|
34893
|
-
this.verbose = config.verbose || false;
|
|
34894
|
-
} else {
|
|
34895
|
-
this.driverType = null;
|
|
34896
|
-
this.driverConfig = {};
|
|
34897
|
-
this.resourceName = config.resourceName || "plg_tfstate_resources";
|
|
34898
|
-
this.stateFilesName = config.stateFilesName || "plg_tfstate_state_files";
|
|
34899
|
-
this.diffsName = config.diffsName || config.stateHistoryName || "plg_tfstate_state_diffs";
|
|
34900
|
-
this.stateHistoryName = this.diffsName;
|
|
34901
|
-
this.autoSync = config.autoSync || false;
|
|
34902
|
-
this.watchPaths = Array.isArray(config.watchPaths) ? config.watchPaths : [];
|
|
34903
|
-
this.filters = config.filters || {};
|
|
34904
|
-
this.trackDiffs = config.trackDiffs !== void 0 ? config.trackDiffs : true;
|
|
34905
|
-
this.diffsLookback = 10;
|
|
34906
|
-
this.asyncPartitions = config.asyncPartitions !== void 0 ? config.asyncPartitions : true;
|
|
34907
|
-
this.verbose = config.verbose || false;
|
|
34908
|
-
this.monitorEnabled = false;
|
|
34909
|
-
this.monitorCron = "*/5 * * * *";
|
|
34910
|
-
}
|
|
33465
|
+
this.driverType = config.driver || null;
|
|
33466
|
+
this.driverConfig = config.config || {};
|
|
33467
|
+
const resources = config.resources || {};
|
|
33468
|
+
this.resourceName = resources.resources || config.resourceName || "plg_tfstate_resources";
|
|
33469
|
+
this.stateFilesName = resources.stateFiles || config.stateFilesName || "plg_tfstate_state_files";
|
|
33470
|
+
this.diffsName = resources.diffs || config.diffsName || "plg_tfstate_state_diffs";
|
|
33471
|
+
const monitor = config.monitor || {};
|
|
33472
|
+
this.monitorEnabled = monitor.enabled || false;
|
|
33473
|
+
this.monitorCron = monitor.cron || "*/5 * * * *";
|
|
33474
|
+
const diffs = config.diffs || {};
|
|
33475
|
+
this.trackDiffs = diffs.enabled !== void 0 ? diffs.enabled : config.trackDiffs !== void 0 ? config.trackDiffs : true;
|
|
33476
|
+
this.diffsLookback = diffs.lookback || 10;
|
|
33477
|
+
this.asyncPartitions = config.asyncPartitions !== void 0 ? config.asyncPartitions : true;
|
|
33478
|
+
this.autoSync = config.autoSync || false;
|
|
33479
|
+
this.watchPaths = config.watchPaths || [];
|
|
33480
|
+
this.filters = config.filters || {};
|
|
33481
|
+
this.verbose = config.verbose || false;
|
|
34911
33482
|
this.supportedVersions = [3, 4];
|
|
34912
33483
|
this.driver = null;
|
|
34913
33484
|
this.resource = null;
|
|
@@ -34957,7 +33528,7 @@ class TfStatePlugin extends Plugin {
|
|
|
34957
33528
|
name: this.lineagesName,
|
|
34958
33529
|
attributes: {
|
|
34959
33530
|
id: "string|required",
|
|
34960
|
-
// = lineage UUID from
|
|
33531
|
+
// = lineage UUID from Tfstate
|
|
34961
33532
|
latestSerial: "number",
|
|
34962
33533
|
// Track latest for quick access
|
|
34963
33534
|
latestStateId: "string",
|
|
@@ -35670,7 +34241,7 @@ class TfStatePlugin extends Plugin {
|
|
|
35670
34241
|
return result;
|
|
35671
34242
|
}
|
|
35672
34243
|
/**
|
|
35673
|
-
* Read and parse
|
|
34244
|
+
* Read and parse Tfstate file
|
|
35674
34245
|
* @private
|
|
35675
34246
|
*/
|
|
35676
34247
|
async _readStateFile(filePath) {
|
|
@@ -35707,7 +34278,7 @@ class TfStatePlugin extends Plugin {
|
|
|
35707
34278
|
}
|
|
35708
34279
|
}
|
|
35709
34280
|
/**
|
|
35710
|
-
* Validate
|
|
34281
|
+
* Validate Tfstate version
|
|
35711
34282
|
* @private
|
|
35712
34283
|
*/
|
|
35713
34284
|
_validateStateVersion(state) {
|
|
@@ -35720,7 +34291,7 @@ class TfStatePlugin extends Plugin {
|
|
|
35720
34291
|
}
|
|
35721
34292
|
}
|
|
35722
34293
|
/**
|
|
35723
|
-
* Extract resources from
|
|
34294
|
+
* Extract resources from Tfstate
|
|
35724
34295
|
* @private
|
|
35725
34296
|
*/
|
|
35726
34297
|
async _extractResources(state, filePath, stateFileId, lineageId) {
|
|
@@ -36287,14 +34858,14 @@ class TfStatePlugin extends Plugin {
|
|
|
36287
34858
|
}
|
|
36288
34859
|
}
|
|
36289
34860
|
/**
|
|
36290
|
-
* Export resources to
|
|
34861
|
+
* Export resources to Tfstate format
|
|
36291
34862
|
* @param {Object} options - Export options
|
|
36292
34863
|
* @param {number} options.serial - Specific serial to export (default: latest)
|
|
36293
34864
|
* @param {string[]} options.resourceTypes - Filter by resource types
|
|
36294
34865
|
* @param {string} options.terraformVersion - Terraform version for output (default: '1.5.0')
|
|
36295
34866
|
* @param {string} options.lineage - State lineage (default: auto-generated)
|
|
36296
34867
|
* @param {Object} options.outputs - Terraform outputs to include
|
|
36297
|
-
* @returns {Promise<Object>}
|
|
34868
|
+
* @returns {Promise<Object>} Tfstate object
|
|
36298
34869
|
*
|
|
36299
34870
|
* @example
|
|
36300
34871
|
* // Export latest state
|
|
@@ -37644,6 +36215,533 @@ class VectorPlugin extends Plugin {
|
|
|
37644
36215
|
}
|
|
37645
36216
|
}
|
|
37646
36217
|
|
|
36218
|
+
function mapFieldTypeToTypeScript(fieldType) {
|
|
36219
|
+
const baseType = fieldType.split("|")[0].trim();
|
|
36220
|
+
const typeMap = {
|
|
36221
|
+
"string": "string",
|
|
36222
|
+
"number": "number",
|
|
36223
|
+
"integer": "number",
|
|
36224
|
+
"boolean": "boolean",
|
|
36225
|
+
"array": "any[]",
|
|
36226
|
+
"object": "Record<string, any>",
|
|
36227
|
+
"json": "Record<string, any>",
|
|
36228
|
+
"secret": "string",
|
|
36229
|
+
"email": "string",
|
|
36230
|
+
"url": "string",
|
|
36231
|
+
"date": "string",
|
|
36232
|
+
// ISO date string
|
|
36233
|
+
"datetime": "string",
|
|
36234
|
+
// ISO datetime string
|
|
36235
|
+
"ip4": "string",
|
|
36236
|
+
"ip6": "string"
|
|
36237
|
+
};
|
|
36238
|
+
if (baseType.startsWith("embedding:")) {
|
|
36239
|
+
const dimensions = parseInt(baseType.split(":")[1]);
|
|
36240
|
+
return `number[] /* ${dimensions} dimensions */`;
|
|
36241
|
+
}
|
|
36242
|
+
return typeMap[baseType] || "any";
|
|
36243
|
+
}
|
|
36244
|
+
function isFieldRequired(fieldDef) {
|
|
36245
|
+
if (typeof fieldDef === "string") {
|
|
36246
|
+
return fieldDef.includes("|required");
|
|
36247
|
+
}
|
|
36248
|
+
if (typeof fieldDef === "object" && fieldDef.required) {
|
|
36249
|
+
return true;
|
|
36250
|
+
}
|
|
36251
|
+
return false;
|
|
36252
|
+
}
|
|
36253
|
+
function generateResourceInterface(resourceName, attributes, timestamps = false) {
|
|
36254
|
+
const interfaceName = toPascalCase(resourceName);
|
|
36255
|
+
const lines = [];
|
|
36256
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
36257
|
+
lines.push(` /** Resource ID (auto-generated) */`);
|
|
36258
|
+
lines.push(` id: string;`);
|
|
36259
|
+
lines.push("");
|
|
36260
|
+
for (const [fieldName, fieldDef] of Object.entries(attributes)) {
|
|
36261
|
+
const required = isFieldRequired(fieldDef);
|
|
36262
|
+
const optional = required ? "" : "?";
|
|
36263
|
+
let tsType;
|
|
36264
|
+
if (typeof fieldDef === "string") {
|
|
36265
|
+
tsType = mapFieldTypeToTypeScript(fieldDef);
|
|
36266
|
+
} else if (typeof fieldDef === "object" && fieldDef.type) {
|
|
36267
|
+
tsType = mapFieldTypeToTypeScript(fieldDef.type);
|
|
36268
|
+
if (fieldDef.type === "object" && fieldDef.props) {
|
|
36269
|
+
tsType = "{\n";
|
|
36270
|
+
for (const [propName, propDef] of Object.entries(fieldDef.props)) {
|
|
36271
|
+
const propType = typeof propDef === "string" ? mapFieldTypeToTypeScript(propDef) : mapFieldTypeToTypeScript(propDef.type);
|
|
36272
|
+
const propRequired = isFieldRequired(propDef);
|
|
36273
|
+
tsType += ` ${propName}${propRequired ? "" : "?"}: ${propType};
|
|
36274
|
+
`;
|
|
36275
|
+
}
|
|
36276
|
+
tsType += " }";
|
|
36277
|
+
}
|
|
36278
|
+
if (fieldDef.type === "array" && fieldDef.items) {
|
|
36279
|
+
const itemType = mapFieldTypeToTypeScript(fieldDef.items);
|
|
36280
|
+
tsType = `Array<${itemType}>`;
|
|
36281
|
+
}
|
|
36282
|
+
} else {
|
|
36283
|
+
tsType = "any";
|
|
36284
|
+
}
|
|
36285
|
+
if (fieldDef.description) {
|
|
36286
|
+
lines.push(` /** ${fieldDef.description} */`);
|
|
36287
|
+
}
|
|
36288
|
+
lines.push(` ${fieldName}${optional}: ${tsType};`);
|
|
36289
|
+
}
|
|
36290
|
+
if (timestamps) {
|
|
36291
|
+
lines.push("");
|
|
36292
|
+
lines.push(` /** Creation timestamp (ISO 8601) */`);
|
|
36293
|
+
lines.push(` createdAt: string;`);
|
|
36294
|
+
lines.push(` /** Last update timestamp (ISO 8601) */`);
|
|
36295
|
+
lines.push(` updatedAt: string;`);
|
|
36296
|
+
}
|
|
36297
|
+
lines.push("}");
|
|
36298
|
+
lines.push("");
|
|
36299
|
+
return lines.join("\n");
|
|
36300
|
+
}
|
|
36301
|
+
function toPascalCase(str) {
|
|
36302
|
+
return str.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
36303
|
+
}
|
|
36304
|
+
async function generateTypes(database, options = {}) {
|
|
36305
|
+
const {
|
|
36306
|
+
outputPath = "./types/database.d.ts",
|
|
36307
|
+
moduleName = "s3db.js",
|
|
36308
|
+
includeResource = true
|
|
36309
|
+
} = options;
|
|
36310
|
+
const lines = [];
|
|
36311
|
+
lines.push("/**");
|
|
36312
|
+
lines.push(" * Auto-generated TypeScript definitions for s3db.js resources");
|
|
36313
|
+
lines.push(" * Generated at: " + (/* @__PURE__ */ new Date()).toISOString());
|
|
36314
|
+
lines.push(" * DO NOT EDIT - This file is auto-generated");
|
|
36315
|
+
lines.push(" */");
|
|
36316
|
+
lines.push("");
|
|
36317
|
+
if (includeResource) {
|
|
36318
|
+
lines.push(`import { Resource, Database } from '${moduleName}';`);
|
|
36319
|
+
lines.push("");
|
|
36320
|
+
}
|
|
36321
|
+
const resourceInterfaces = [];
|
|
36322
|
+
for (const [name, resource] of Object.entries(database.resources)) {
|
|
36323
|
+
const attributes = resource.config?.attributes || resource.attributes || {};
|
|
36324
|
+
const timestamps = resource.config?.timestamps || false;
|
|
36325
|
+
const interfaceDef = generateResourceInterface(name, attributes, timestamps);
|
|
36326
|
+
lines.push(interfaceDef);
|
|
36327
|
+
resourceInterfaces.push({
|
|
36328
|
+
name,
|
|
36329
|
+
interfaceName: toPascalCase(name),
|
|
36330
|
+
resource
|
|
36331
|
+
});
|
|
36332
|
+
}
|
|
36333
|
+
lines.push("/**");
|
|
36334
|
+
lines.push(" * Typed resource map for property access");
|
|
36335
|
+
lines.push(" * @example");
|
|
36336
|
+
lines.push(" * const users = db.resources.users; // Type-safe!");
|
|
36337
|
+
lines.push(' * const user = await users.get("id"); // Autocomplete works!');
|
|
36338
|
+
lines.push(" */");
|
|
36339
|
+
lines.push("export interface ResourceMap {");
|
|
36340
|
+
for (const { name, interfaceName } of resourceInterfaces) {
|
|
36341
|
+
lines.push(` /** ${interfaceName} resource */`);
|
|
36342
|
+
if (includeResource) {
|
|
36343
|
+
lines.push(` ${name}: Resource<${interfaceName}>;`);
|
|
36344
|
+
} else {
|
|
36345
|
+
lines.push(` ${name}: any;`);
|
|
36346
|
+
}
|
|
36347
|
+
}
|
|
36348
|
+
lines.push("}");
|
|
36349
|
+
lines.push("");
|
|
36350
|
+
if (includeResource) {
|
|
36351
|
+
lines.push("/**");
|
|
36352
|
+
lines.push(" * Extended Database class with typed resources");
|
|
36353
|
+
lines.push(" */");
|
|
36354
|
+
lines.push("declare module 's3db.js' {");
|
|
36355
|
+
lines.push(" interface Database {");
|
|
36356
|
+
lines.push(" resources: ResourceMap;");
|
|
36357
|
+
lines.push(" }");
|
|
36358
|
+
lines.push("");
|
|
36359
|
+
lines.push(" interface Resource<T = any> {");
|
|
36360
|
+
lines.push(" get(id: string): Promise<T>;");
|
|
36361
|
+
lines.push(" getOrNull(id: string): Promise<T | null>;");
|
|
36362
|
+
lines.push(" getOrThrow(id: string): Promise<T>;");
|
|
36363
|
+
lines.push(" insert(data: Partial<T>): Promise<T>;");
|
|
36364
|
+
lines.push(" update(id: string, data: Partial<T>): Promise<T>;");
|
|
36365
|
+
lines.push(" patch(id: string, data: Partial<T>): Promise<T>;");
|
|
36366
|
+
lines.push(" replace(id: string, data: Partial<T>): Promise<T>;");
|
|
36367
|
+
lines.push(" delete(id: string): Promise<void>;");
|
|
36368
|
+
lines.push(" list(options?: any): Promise<T[]>;");
|
|
36369
|
+
lines.push(" query(filters: Partial<T>, options?: any): Promise<T[]>;");
|
|
36370
|
+
lines.push(" validate(data: Partial<T>, options?: any): Promise<{ valid: boolean; errors: any[]; data: T | null }>;");
|
|
36371
|
+
lines.push(" }");
|
|
36372
|
+
lines.push("}");
|
|
36373
|
+
}
|
|
36374
|
+
const content = lines.join("\n");
|
|
36375
|
+
if (outputPath) {
|
|
36376
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
36377
|
+
await writeFile(outputPath, content, "utf-8");
|
|
36378
|
+
}
|
|
36379
|
+
return content;
|
|
36380
|
+
}
|
|
36381
|
+
async function printTypes(database, options = {}) {
|
|
36382
|
+
const types = await generateTypes(database, { ...options, outputPath: null });
|
|
36383
|
+
console.log(types);
|
|
36384
|
+
return types;
|
|
36385
|
+
}
|
|
36386
|
+
|
|
36387
|
+
class Factory {
|
|
36388
|
+
/**
|
|
36389
|
+
* Global sequence counter
|
|
36390
|
+
* @private
|
|
36391
|
+
*/
|
|
36392
|
+
static _sequences = /* @__PURE__ */ new Map();
|
|
36393
|
+
/**
|
|
36394
|
+
* Registered factories
|
|
36395
|
+
* @private
|
|
36396
|
+
*/
|
|
36397
|
+
static _factories = /* @__PURE__ */ new Map();
|
|
36398
|
+
/**
|
|
36399
|
+
* Database instance (set globally)
|
|
36400
|
+
* @private
|
|
36401
|
+
*/
|
|
36402
|
+
static _database = null;
|
|
36403
|
+
/**
|
|
36404
|
+
* Create a new factory definition
|
|
36405
|
+
* @param {string} resourceName - Resource name
|
|
36406
|
+
* @param {Object|Function} definition - Field definitions or function
|
|
36407
|
+
* @param {Object} options - Factory options
|
|
36408
|
+
* @returns {Factory} Factory instance
|
|
36409
|
+
*/
|
|
36410
|
+
static define(resourceName, definition, options = {}) {
|
|
36411
|
+
const factory = new Factory(resourceName, definition, options);
|
|
36412
|
+
Factory._factories.set(resourceName, factory);
|
|
36413
|
+
return factory;
|
|
36414
|
+
}
|
|
36415
|
+
/**
|
|
36416
|
+
* Set global database instance
|
|
36417
|
+
* @param {Database} database - s3db.js Database instance
|
|
36418
|
+
*/
|
|
36419
|
+
static setDatabase(database) {
|
|
36420
|
+
Factory._database = database;
|
|
36421
|
+
}
|
|
36422
|
+
/**
|
|
36423
|
+
* Get factory by resource name
|
|
36424
|
+
* @param {string} resourceName - Resource name
|
|
36425
|
+
* @returns {Factory} Factory instance
|
|
36426
|
+
*/
|
|
36427
|
+
static get(resourceName) {
|
|
36428
|
+
return Factory._factories.get(resourceName);
|
|
36429
|
+
}
|
|
36430
|
+
/**
|
|
36431
|
+
* Reset all sequences
|
|
36432
|
+
*/
|
|
36433
|
+
static resetSequences() {
|
|
36434
|
+
Factory._sequences.clear();
|
|
36435
|
+
}
|
|
36436
|
+
/**
|
|
36437
|
+
* Reset all factories
|
|
36438
|
+
*/
|
|
36439
|
+
static reset() {
|
|
36440
|
+
Factory._sequences.clear();
|
|
36441
|
+
Factory._factories.clear();
|
|
36442
|
+
Factory._database = null;
|
|
36443
|
+
}
|
|
36444
|
+
/**
|
|
36445
|
+
* Constructor
|
|
36446
|
+
* @param {string} resourceName - Resource name
|
|
36447
|
+
* @param {Object|Function} definition - Field definitions
|
|
36448
|
+
* @param {Object} options - Factory options
|
|
36449
|
+
*/
|
|
36450
|
+
constructor(resourceName, definition, options = {}) {
|
|
36451
|
+
this.resourceName = resourceName;
|
|
36452
|
+
this.definition = definition;
|
|
36453
|
+
this.options = options;
|
|
36454
|
+
this.traits = /* @__PURE__ */ new Map();
|
|
36455
|
+
this.afterCreateCallbacks = [];
|
|
36456
|
+
this.beforeCreateCallbacks = [];
|
|
36457
|
+
}
|
|
36458
|
+
/**
|
|
36459
|
+
* Get next sequence number
|
|
36460
|
+
* @param {string} name - Sequence name (default: factory name)
|
|
36461
|
+
* @returns {number} Next sequence number
|
|
36462
|
+
*/
|
|
36463
|
+
sequence(name = this.resourceName) {
|
|
36464
|
+
const current = Factory._sequences.get(name) || 0;
|
|
36465
|
+
const next = current + 1;
|
|
36466
|
+
Factory._sequences.set(name, next);
|
|
36467
|
+
return next;
|
|
36468
|
+
}
|
|
36469
|
+
/**
|
|
36470
|
+
* Define a trait (state variation)
|
|
36471
|
+
* @param {string} name - Trait name
|
|
36472
|
+
* @param {Object|Function} attributes - Trait attributes
|
|
36473
|
+
* @returns {Factory} This factory (for chaining)
|
|
36474
|
+
*/
|
|
36475
|
+
trait(name, attributes) {
|
|
36476
|
+
this.traits.set(name, attributes);
|
|
36477
|
+
return this;
|
|
36478
|
+
}
|
|
36479
|
+
/**
|
|
36480
|
+
* Register after create callback
|
|
36481
|
+
* @param {Function} callback - Callback function
|
|
36482
|
+
* @returns {Factory} This factory (for chaining)
|
|
36483
|
+
*/
|
|
36484
|
+
afterCreate(callback) {
|
|
36485
|
+
this.afterCreateCallbacks.push(callback);
|
|
36486
|
+
return this;
|
|
36487
|
+
}
|
|
36488
|
+
/**
|
|
36489
|
+
* Register before create callback
|
|
36490
|
+
* @param {Function} callback - Callback function
|
|
36491
|
+
* @returns {Factory} This factory (for chaining)
|
|
36492
|
+
*/
|
|
36493
|
+
beforeCreate(callback) {
|
|
36494
|
+
this.beforeCreateCallbacks.push(callback);
|
|
36495
|
+
return this;
|
|
36496
|
+
}
|
|
36497
|
+
/**
|
|
36498
|
+
* Build attributes without creating in database
|
|
36499
|
+
* @param {Object} overrides - Override attributes
|
|
36500
|
+
* @param {Object} options - Build options
|
|
36501
|
+
* @returns {Promise<Object>} Built attributes
|
|
36502
|
+
*/
|
|
36503
|
+
async build(overrides = {}, options = {}) {
|
|
36504
|
+
const { traits = [] } = options;
|
|
36505
|
+
const seq = this.sequence();
|
|
36506
|
+
let attributes = typeof this.definition === "function" ? await this.definition({ seq, factory: this }) : { ...this.definition };
|
|
36507
|
+
for (const traitName of traits) {
|
|
36508
|
+
const trait = this.traits.get(traitName);
|
|
36509
|
+
if (!trait) {
|
|
36510
|
+
throw new Error(`Trait '${traitName}' not found in factory '${this.resourceName}'`);
|
|
36511
|
+
}
|
|
36512
|
+
const traitAttrs = typeof trait === "function" ? await trait({ seq, factory: this }) : trait;
|
|
36513
|
+
attributes = { ...attributes, ...traitAttrs };
|
|
36514
|
+
}
|
|
36515
|
+
attributes = { ...attributes, ...overrides };
|
|
36516
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
36517
|
+
if (typeof value === "function") {
|
|
36518
|
+
attributes[key] = await value({ seq, factory: this });
|
|
36519
|
+
}
|
|
36520
|
+
}
|
|
36521
|
+
return attributes;
|
|
36522
|
+
}
|
|
36523
|
+
/**
|
|
36524
|
+
* Create resource in database
|
|
36525
|
+
* @param {Object} overrides - Override attributes
|
|
36526
|
+
* @param {Object} options - Create options
|
|
36527
|
+
* @returns {Promise<Object>} Created resource
|
|
36528
|
+
*/
|
|
36529
|
+
async create(overrides = {}, options = {}) {
|
|
36530
|
+
const { database = Factory._database } = options;
|
|
36531
|
+
if (!database) {
|
|
36532
|
+
throw new Error("Database not set. Use Factory.setDatabase(db) or pass database option");
|
|
36533
|
+
}
|
|
36534
|
+
let attributes = await this.build(overrides, options);
|
|
36535
|
+
for (const callback of this.beforeCreateCallbacks) {
|
|
36536
|
+
attributes = await callback(attributes) || attributes;
|
|
36537
|
+
}
|
|
36538
|
+
const resource = database.resources[this.resourceName];
|
|
36539
|
+
if (!resource) {
|
|
36540
|
+
throw new Error(`Resource '${this.resourceName}' not found in database`);
|
|
36541
|
+
}
|
|
36542
|
+
let created = await resource.insert(attributes);
|
|
36543
|
+
for (const callback of this.afterCreateCallbacks) {
|
|
36544
|
+
created = await callback(created, { database }) || created;
|
|
36545
|
+
}
|
|
36546
|
+
return created;
|
|
36547
|
+
}
|
|
36548
|
+
/**
|
|
36549
|
+
* Create multiple resources
|
|
36550
|
+
* @param {number} count - Number of resources to create
|
|
36551
|
+
* @param {Object} overrides - Override attributes
|
|
36552
|
+
* @param {Object} options - Create options
|
|
36553
|
+
* @returns {Promise<Object[]>} Created resources
|
|
36554
|
+
*/
|
|
36555
|
+
async createMany(count, overrides = {}, options = {}) {
|
|
36556
|
+
const resources = [];
|
|
36557
|
+
for (let i = 0; i < count; i++) {
|
|
36558
|
+
const resource = await this.create(overrides, options);
|
|
36559
|
+
resources.push(resource);
|
|
36560
|
+
}
|
|
36561
|
+
return resources;
|
|
36562
|
+
}
|
|
36563
|
+
/**
|
|
36564
|
+
* Build multiple resources without creating
|
|
36565
|
+
* @param {number} count - Number of resources to build
|
|
36566
|
+
* @param {Object} overrides - Override attributes
|
|
36567
|
+
* @param {Object} options - Build options
|
|
36568
|
+
* @returns {Promise<Object[]>} Built resources
|
|
36569
|
+
*/
|
|
36570
|
+
async buildMany(count, overrides = {}, options = {}) {
|
|
36571
|
+
const resources = [];
|
|
36572
|
+
for (let i = 0; i < count; i++) {
|
|
36573
|
+
const resource = await this.build(overrides, options);
|
|
36574
|
+
resources.push(resource);
|
|
36575
|
+
}
|
|
36576
|
+
return resources;
|
|
36577
|
+
}
|
|
36578
|
+
/**
|
|
36579
|
+
* Create with specific traits
|
|
36580
|
+
* @param {string|string[]} traits - Trait name(s)
|
|
36581
|
+
* @param {Object} overrides - Override attributes
|
|
36582
|
+
* @param {Object} options - Create options
|
|
36583
|
+
* @returns {Promise<Object>} Created resource
|
|
36584
|
+
*/
|
|
36585
|
+
async createWithTraits(traits, overrides = {}, options = {}) {
|
|
36586
|
+
const traitArray = Array.isArray(traits) ? traits : [traits];
|
|
36587
|
+
return this.create(overrides, { ...options, traits: traitArray });
|
|
36588
|
+
}
|
|
36589
|
+
/**
|
|
36590
|
+
* Build with specific traits
|
|
36591
|
+
* @param {string|string[]} traits - Trait name(s)
|
|
36592
|
+
* @param {Object} overrides - Override attributes
|
|
36593
|
+
* @param {Object} options - Build options
|
|
36594
|
+
* @returns {Promise<Object>} Built resource
|
|
36595
|
+
*/
|
|
36596
|
+
async buildWithTraits(traits, overrides = {}, options = {}) {
|
|
36597
|
+
const traitArray = Array.isArray(traits) ? traits : [traits];
|
|
36598
|
+
return this.build(overrides, { ...options, traits: traitArray });
|
|
36599
|
+
}
|
|
36600
|
+
}
|
|
36601
|
+
|
|
36602
|
+
class Seeder {
|
|
36603
|
+
/**
|
|
36604
|
+
* Constructor
|
|
36605
|
+
* @param {Database} database - s3db.js Database instance
|
|
36606
|
+
* @param {Object} options - Seeder options
|
|
36607
|
+
*/
|
|
36608
|
+
constructor(database, options = {}) {
|
|
36609
|
+
this.database = database;
|
|
36610
|
+
this.options = options;
|
|
36611
|
+
this.verbose = options.verbose !== false;
|
|
36612
|
+
}
|
|
36613
|
+
/**
|
|
36614
|
+
* Log message (if verbose)
|
|
36615
|
+
* @param {string} message - Message to log
|
|
36616
|
+
* @private
|
|
36617
|
+
*/
|
|
36618
|
+
log(message) {
|
|
36619
|
+
if (this.verbose) {
|
|
36620
|
+
console.log(`[Seeder] ${message}`);
|
|
36621
|
+
}
|
|
36622
|
+
}
|
|
36623
|
+
/**
|
|
36624
|
+
* Seed resources using factories
|
|
36625
|
+
* @param {Object} specs - Seed specifications { resourceName: count }
|
|
36626
|
+
* @returns {Promise<Object>} Created resources by resource name
|
|
36627
|
+
*
|
|
36628
|
+
* @example
|
|
36629
|
+
* const created = await seeder.seed({
|
|
36630
|
+
* users: 10,
|
|
36631
|
+
* posts: 50
|
|
36632
|
+
* });
|
|
36633
|
+
*/
|
|
36634
|
+
async seed(specs) {
|
|
36635
|
+
const created = {};
|
|
36636
|
+
for (const [resourceName, count] of Object.entries(specs)) {
|
|
36637
|
+
this.log(`Seeding ${count} ${resourceName}...`);
|
|
36638
|
+
const factory = Factory.get(resourceName);
|
|
36639
|
+
if (!factory) {
|
|
36640
|
+
throw new Error(`Factory for '${resourceName}' not found. Define it with Factory.define()`);
|
|
36641
|
+
}
|
|
36642
|
+
created[resourceName] = await factory.createMany(count, {}, { database: this.database });
|
|
36643
|
+
this.log(`\u2705 Created ${count} ${resourceName}`);
|
|
36644
|
+
}
|
|
36645
|
+
return created;
|
|
36646
|
+
}
|
|
36647
|
+
/**
|
|
36648
|
+
* Seed with custom callback
|
|
36649
|
+
* @param {Function} callback - Seeding callback
|
|
36650
|
+
* @returns {Promise<any>} Result of callback
|
|
36651
|
+
*
|
|
36652
|
+
* @example
|
|
36653
|
+
* await seeder.call(async (db) => {
|
|
36654
|
+
* const user = await UserFactory.create();
|
|
36655
|
+
* const posts = await PostFactory.createMany(5, { userId: user.id });
|
|
36656
|
+
* return { user, posts };
|
|
36657
|
+
* });
|
|
36658
|
+
*/
|
|
36659
|
+
async call(callback) {
|
|
36660
|
+
this.log("Running custom seeder...");
|
|
36661
|
+
const result = await callback(this.database);
|
|
36662
|
+
this.log("\u2705 Custom seeder completed");
|
|
36663
|
+
return result;
|
|
36664
|
+
}
|
|
36665
|
+
/**
|
|
36666
|
+
* Truncate resources (delete all data)
|
|
36667
|
+
* @param {string[]} resourceNames - Resource names to truncate
|
|
36668
|
+
* @returns {Promise<void>}
|
|
36669
|
+
*
|
|
36670
|
+
* @example
|
|
36671
|
+
* await seeder.truncate(['users', 'posts']);
|
|
36672
|
+
*/
|
|
36673
|
+
async truncate(resourceNames) {
|
|
36674
|
+
for (const resourceName of resourceNames) {
|
|
36675
|
+
this.log(`Truncating ${resourceName}...`);
|
|
36676
|
+
const resource = this.database.resources[resourceName];
|
|
36677
|
+
if (!resource) {
|
|
36678
|
+
this.log(`\u26A0\uFE0F Resource '${resourceName}' not found, skipping`);
|
|
36679
|
+
continue;
|
|
36680
|
+
}
|
|
36681
|
+
const ids = await resource.listIds();
|
|
36682
|
+
if (ids.length > 0) {
|
|
36683
|
+
await resource.deleteMany(ids);
|
|
36684
|
+
this.log(`\u2705 Deleted ${ids.length} ${resourceName}`);
|
|
36685
|
+
} else {
|
|
36686
|
+
this.log(`\u2705 ${resourceName} already empty`);
|
|
36687
|
+
}
|
|
36688
|
+
}
|
|
36689
|
+
}
|
|
36690
|
+
/**
|
|
36691
|
+
* Truncate all resources
|
|
36692
|
+
* @returns {Promise<void>}
|
|
36693
|
+
*/
|
|
36694
|
+
async truncateAll() {
|
|
36695
|
+
const resourceNames = Object.keys(this.database.resources);
|
|
36696
|
+
await this.truncate(resourceNames);
|
|
36697
|
+
}
|
|
36698
|
+
/**
|
|
36699
|
+
* Run multiple seeders in order
|
|
36700
|
+
* @param {Function[]} seeders - Array of seeder functions
|
|
36701
|
+
* @returns {Promise<Object[]>} Results of each seeder
|
|
36702
|
+
*
|
|
36703
|
+
* @example
|
|
36704
|
+
* await seeder.run([
|
|
36705
|
+
* async (db) => await UserFactory.createMany(10),
|
|
36706
|
+
* async (db) => await PostFactory.createMany(50)
|
|
36707
|
+
* ]);
|
|
36708
|
+
*/
|
|
36709
|
+
async run(seeders) {
|
|
36710
|
+
const results = [];
|
|
36711
|
+
for (const seederFn of seeders) {
|
|
36712
|
+
this.log(`Running seeder ${seederFn.name || "anonymous"}...`);
|
|
36713
|
+
const result = await seederFn(this.database);
|
|
36714
|
+
results.push(result);
|
|
36715
|
+
this.log(`\u2705 Completed ${seederFn.name || "anonymous"}`);
|
|
36716
|
+
}
|
|
36717
|
+
return results;
|
|
36718
|
+
}
|
|
36719
|
+
/**
|
|
36720
|
+
* Seed and return specific resources
|
|
36721
|
+
* @param {Object} specs - Seed specifications
|
|
36722
|
+
* @returns {Promise<Object>} Created resources
|
|
36723
|
+
*
|
|
36724
|
+
* @example
|
|
36725
|
+
* const { users, posts } = await seeder.seedAndReturn({
|
|
36726
|
+
* users: 5,
|
|
36727
|
+
* posts: 10
|
|
36728
|
+
* });
|
|
36729
|
+
*/
|
|
36730
|
+
async seedAndReturn(specs) {
|
|
36731
|
+
return await this.seed(specs);
|
|
36732
|
+
}
|
|
36733
|
+
/**
|
|
36734
|
+
* Reset database (truncate all and reset sequences)
|
|
36735
|
+
* @returns {Promise<void>}
|
|
36736
|
+
*/
|
|
36737
|
+
async reset() {
|
|
36738
|
+
this.log("Resetting database...");
|
|
36739
|
+
await this.truncateAll();
|
|
36740
|
+
Factory.resetSequences();
|
|
36741
|
+
this.log("\u2705 Database reset complete");
|
|
36742
|
+
}
|
|
36743
|
+
}
|
|
36744
|
+
|
|
37647
36745
|
function sanitizeLabel(value) {
|
|
37648
36746
|
if (typeof value !== "string") {
|
|
37649
36747
|
value = String(value);
|
|
@@ -38018,5 +37116,5 @@ var metrics = /*#__PURE__*/Object.freeze({
|
|
|
38018
37116
|
silhouetteScore: silhouetteScore
|
|
38019
37117
|
});
|
|
38020
37118
|
|
|
38021
|
-
export { AVAILABLE_BEHAVIORS, AnalyticsNotEnabledError, ApiPlugin, AuditPlugin, AuthenticationError, BACKUP_DRIVERS, BackupPlugin, BaseBackupDriver, BaseError, BaseReplicator, BehaviorError, BigqueryReplicator, CONSUMER_DRIVERS, Cache, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, DynamoDBReplicator, EncryptionError, ErrorMap, EventualConsistencyPlugin, FilesystemBackupDriver, FilesystemCache, FullTextPlugin, InvalidResourceItem, MemoryCache, MetadataLimitError, MetricsPlugin, MissingMetadata, MongoDBReplicator, MultiBackupDriver, MySQLReplicator, NoSuchBucket, NoSuchKey, NotFound, PartitionAwareFilesystemCache, PartitionDriverError, PartitionError, PermissionError, PlanetScaleReplicator, Plugin, PluginError, PluginObject, PluginStorageError, PostgresReplicator, QueueConsumerPlugin, REPLICATOR_DRIVERS, RabbitMqConsumer, RelationPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3BackupDriver, S3Cache, S3QueuePlugin, Database as S3db, S3dbError, S3dbReplicator, SchedulerPlugin, Schema, SchemaError, SqsConsumer, SqsReplicator, StateMachinePlugin, StreamError, TfStatePlugin, TursoReplicator, UnknownError, ValidationError, Validator, VectorPlugin, WebhookReplicator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes,
|
|
37119
|
+
export { AVAILABLE_BEHAVIORS, AnalyticsNotEnabledError, ApiPlugin, AuditPlugin, AuthenticationError, BACKUP_DRIVERS, BackupPlugin, BaseBackupDriver, BaseError, BaseReplicator, BehaviorError, BigqueryReplicator, CONSUMER_DRIVERS, Cache, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, DynamoDBReplicator, EncryptionError, ErrorMap, EventualConsistencyPlugin, Factory, FilesystemBackupDriver, FilesystemCache, FullTextPlugin, InvalidResourceItem, MemoryCache, MetadataLimitError, MetricsPlugin, MissingMetadata, MongoDBReplicator, MultiBackupDriver, MySQLReplicator, NoSuchBucket, NoSuchKey, NotFound, PartitionAwareFilesystemCache, PartitionDriverError, PartitionError, PermissionError, PlanetScaleReplicator, Plugin, PluginError, PluginObject, PluginStorageError, PostgresReplicator, QueueConsumerPlugin, REPLICATOR_DRIVERS, RabbitMqConsumer, RelationPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3BackupDriver, S3Cache, S3QueuePlugin, Database as S3db, S3dbError, S3dbReplicator, SchedulerPlugin, Schema, SchemaError, Seeder, SqsConsumer, SqsReplicator, StateMachinePlugin, StreamError, TfStatePlugin, TursoReplicator, UnknownError, ValidationError, Validator, VectorPlugin, WebhookReplicator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes, clearUTF8Memory, createBackupDriver, createConsumer, createReplicator, decode, decodeDecimal, decodeFixedPoint, decodeFixedPointBatch, decrypt, S3db as default, encode, encodeDecimal, encodeFixedPoint, encodeFixedPointBatch, encrypt, generateTypes, getBehavior, getSizeBreakdown, idGenerator, mapAwsError, md5, passwordGenerator, printTypes, sha256, streamToString, transformValue, tryFn, tryFnSync, validateBackupConfig, validateReplicatorConfig };
|
|
38022
37120
|
//# sourceMappingURL=s3db.es.js.map
|