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.cjs.js
CHANGED
|
@@ -5,9 +5,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var crypto$1 = require('crypto');
|
|
6
6
|
var nanoid = require('nanoid');
|
|
7
7
|
var EventEmitter = require('events');
|
|
8
|
-
var hono = require('hono');
|
|
9
|
-
var nodeServer = require('@hono/node-server');
|
|
10
|
-
var swaggerUi = require('@hono/swagger-ui');
|
|
11
8
|
var promises = require('fs/promises');
|
|
12
9
|
var fs = require('fs');
|
|
13
10
|
var promises$1 = require('stream/promises');
|
|
@@ -232,8 +229,6 @@ function calculateUTF8Bytes(str) {
|
|
|
232
229
|
function clearUTF8Memory() {
|
|
233
230
|
utf8BytesMemory.clear();
|
|
234
231
|
}
|
|
235
|
-
const clearUTF8Memo = clearUTF8Memory;
|
|
236
|
-
const clearUTF8Cache = clearUTF8Memory;
|
|
237
232
|
function calculateAttributeNamesSize(mappedObject) {
|
|
238
233
|
let totalSize = 0;
|
|
239
234
|
for (const key of Object.keys(mappedObject)) {
|
|
@@ -1627,18 +1622,6 @@ function metadataDecode(value) {
|
|
|
1627
1622
|
}
|
|
1628
1623
|
}
|
|
1629
1624
|
}
|
|
1630
|
-
const len = value.length;
|
|
1631
|
-
if (len > 0 && len % 4 === 0) {
|
|
1632
|
-
if (/^[A-Za-z0-9+/]+=*$/.test(value)) {
|
|
1633
|
-
try {
|
|
1634
|
-
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
1635
|
-
if (/[^\x00-\x7F]/.test(decoded) && Buffer.from(decoded, "utf8").toString("base64") === value) {
|
|
1636
|
-
return decoded;
|
|
1637
|
-
}
|
|
1638
|
-
} catch {
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
1625
|
return value;
|
|
1643
1626
|
}
|
|
1644
1627
|
|
|
@@ -1725,11 +1708,22 @@ class PluginStorage {
|
|
|
1725
1708
|
}
|
|
1726
1709
|
}
|
|
1727
1710
|
/**
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1711
|
+
* Batch set multiple items
|
|
1712
|
+
*
|
|
1713
|
+
* @param {Array<{key: string, data: Object, options?: Object}>} items - Items to save
|
|
1714
|
+
* @returns {Promise<Array<{ok: boolean, key: string, error?: Error}>>} Results
|
|
1730
1715
|
*/
|
|
1731
|
-
async
|
|
1732
|
-
|
|
1716
|
+
async batchSet(items) {
|
|
1717
|
+
const results = [];
|
|
1718
|
+
for (const item of items) {
|
|
1719
|
+
try {
|
|
1720
|
+
await this.set(item.key, item.data, item.options || {});
|
|
1721
|
+
results.push({ ok: true, key: item.key });
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
results.push({ ok: false, key: item.key, error });
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return results;
|
|
1733
1727
|
}
|
|
1734
1728
|
/**
|
|
1735
1729
|
* Get data with automatic metadata decoding and TTL check
|
|
@@ -2477,1800 +2471,100 @@ const PluginObject = {
|
|
|
2477
2471
|
}
|
|
2478
2472
|
};
|
|
2479
2473
|
|
|
2480
|
-
function success(data, options = {}) {
|
|
2481
|
-
const { status = 200, meta = {} } = options;
|
|
2482
|
-
return {
|
|
2483
|
-
success: true,
|
|
2484
|
-
data,
|
|
2485
|
-
meta: {
|
|
2486
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2487
|
-
...meta
|
|
2488
|
-
},
|
|
2489
|
-
_status: status
|
|
2490
|
-
};
|
|
2491
|
-
}
|
|
2492
|
-
function error(error2, options = {}) {
|
|
2493
|
-
const { status = 500, code = "INTERNAL_ERROR", details = {} } = options;
|
|
2494
|
-
const errorMessage = error2 instanceof Error ? error2.message : error2;
|
|
2495
|
-
const errorStack = error2 instanceof Error && process.env.NODE_ENV !== "production" ? error2.stack : void 0;
|
|
2496
|
-
return {
|
|
2497
|
-
success: false,
|
|
2498
|
-
error: {
|
|
2499
|
-
message: errorMessage,
|
|
2500
|
-
code,
|
|
2501
|
-
details,
|
|
2502
|
-
stack: errorStack
|
|
2503
|
-
},
|
|
2504
|
-
meta: {
|
|
2505
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2506
|
-
},
|
|
2507
|
-
_status: status
|
|
2508
|
-
};
|
|
2509
|
-
}
|
|
2510
|
-
function list(items, pagination = {}) {
|
|
2511
|
-
const { total, page, pageSize, pageCount } = pagination;
|
|
2512
|
-
return {
|
|
2513
|
-
success: true,
|
|
2514
|
-
data: items,
|
|
2515
|
-
pagination: {
|
|
2516
|
-
total: total || items.length,
|
|
2517
|
-
page: page || 1,
|
|
2518
|
-
pageSize: pageSize || items.length,
|
|
2519
|
-
pageCount: pageCount || 1
|
|
2520
|
-
},
|
|
2521
|
-
meta: {
|
|
2522
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2523
|
-
},
|
|
2524
|
-
_status: 200
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
function created(data, location) {
|
|
2528
|
-
return {
|
|
2529
|
-
success: true,
|
|
2530
|
-
data,
|
|
2531
|
-
meta: {
|
|
2532
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2533
|
-
location
|
|
2534
|
-
},
|
|
2535
|
-
_status: 201
|
|
2536
|
-
};
|
|
2537
|
-
}
|
|
2538
|
-
function noContent() {
|
|
2539
|
-
return {
|
|
2540
|
-
success: true,
|
|
2541
|
-
data: null,
|
|
2542
|
-
meta: {
|
|
2543
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2544
|
-
},
|
|
2545
|
-
_status: 204
|
|
2546
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
function notFound(resource, id) {
|
|
2549
|
-
return error(`${resource} with id '${id}' not found`, {
|
|
2550
|
-
status: 404,
|
|
2551
|
-
code: "NOT_FOUND",
|
|
2552
|
-
details: { resource, id }
|
|
2553
|
-
});
|
|
2554
|
-
}
|
|
2555
|
-
function payloadTooLarge(size, limit) {
|
|
2556
|
-
return error("Request payload too large", {
|
|
2557
|
-
status: 413,
|
|
2558
|
-
code: "PAYLOAD_TOO_LARGE",
|
|
2559
|
-
details: {
|
|
2560
|
-
receivedSize: size,
|
|
2561
|
-
maxSize: limit,
|
|
2562
|
-
receivedMB: (size / 1024 / 1024).toFixed(2),
|
|
2563
|
-
maxMB: (limit / 1024 / 1024).toFixed(2)
|
|
2564
|
-
}
|
|
2565
|
-
});
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
const errorStatusMap = {
|
|
2569
|
-
"ValidationError": 400,
|
|
2570
|
-
"InvalidResourceItem": 400,
|
|
2571
|
-
"ResourceNotFound": 404,
|
|
2572
|
-
"NoSuchKey": 404,
|
|
2573
|
-
"NoSuchBucket": 404,
|
|
2574
|
-
"PartitionError": 400,
|
|
2575
|
-
"CryptoError": 500,
|
|
2576
|
-
"SchemaError": 400,
|
|
2577
|
-
"QueueError": 500,
|
|
2578
|
-
"ResourceError": 500
|
|
2579
|
-
};
|
|
2580
|
-
function getStatusFromError(err) {
|
|
2581
|
-
if (err.name && errorStatusMap[err.name]) {
|
|
2582
|
-
return errorStatusMap[err.name];
|
|
2583
|
-
}
|
|
2584
|
-
if (err.constructor && err.constructor.name && errorStatusMap[err.constructor.name]) {
|
|
2585
|
-
return errorStatusMap[err.constructor.name];
|
|
2586
|
-
}
|
|
2587
|
-
if (err.message) {
|
|
2588
|
-
if (err.message.includes("not found") || err.message.includes("does not exist")) {
|
|
2589
|
-
return 404;
|
|
2590
|
-
}
|
|
2591
|
-
if (err.message.includes("validation") || err.message.includes("invalid")) {
|
|
2592
|
-
return 400;
|
|
2593
|
-
}
|
|
2594
|
-
if (err.message.includes("unauthorized") || err.message.includes("authentication")) {
|
|
2595
|
-
return 401;
|
|
2596
|
-
}
|
|
2597
|
-
if (err.message.includes("forbidden") || err.message.includes("permission")) {
|
|
2598
|
-
return 403;
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
return 500;
|
|
2602
|
-
}
|
|
2603
|
-
function errorHandler(err, c) {
|
|
2604
|
-
const status = getStatusFromError(err);
|
|
2605
|
-
const code = err.name || "INTERNAL_ERROR";
|
|
2606
|
-
const details = {};
|
|
2607
|
-
if (err.resource) details.resource = err.resource;
|
|
2608
|
-
if (err.bucket) details.bucket = err.bucket;
|
|
2609
|
-
if (err.key) details.key = err.key;
|
|
2610
|
-
if (err.operation) details.operation = err.operation;
|
|
2611
|
-
if (err.suggestion) details.suggestion = err.suggestion;
|
|
2612
|
-
if (err.availableResources) details.availableResources = err.availableResources;
|
|
2613
|
-
const response = error(err, {
|
|
2614
|
-
status,
|
|
2615
|
-
code,
|
|
2616
|
-
details
|
|
2617
|
-
});
|
|
2618
|
-
if (status >= 500) {
|
|
2619
|
-
console.error("[API Plugin] Error:", {
|
|
2620
|
-
message: err.message,
|
|
2621
|
-
code,
|
|
2622
|
-
status,
|
|
2623
|
-
stack: err.stack,
|
|
2624
|
-
details
|
|
2625
|
-
});
|
|
2626
|
-
} else if (status >= 400 && status < 500 && c.get("verbose")) {
|
|
2627
|
-
console.warn("[API Plugin] Client error:", {
|
|
2628
|
-
message: err.message,
|
|
2629
|
-
code,
|
|
2630
|
-
status,
|
|
2631
|
-
details
|
|
2632
|
-
});
|
|
2633
|
-
}
|
|
2634
|
-
return c.json(response, response._status);
|
|
2635
|
-
}
|
|
2636
|
-
function asyncHandler(fn) {
|
|
2637
|
-
return async (c) => {
|
|
2638
|
-
try {
|
|
2639
|
-
return await fn(c);
|
|
2640
|
-
} catch (err) {
|
|
2641
|
-
return errorHandler(err, c);
|
|
2642
|
-
}
|
|
2643
|
-
};
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
function createResourceRoutes(resource, version, config = {}) {
|
|
2647
|
-
const app = new hono.Hono();
|
|
2648
|
-
const {
|
|
2649
|
-
methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
2650
|
-
customMiddleware = [],
|
|
2651
|
-
enableValidation = true
|
|
2652
|
-
} = config;
|
|
2653
|
-
const resourceName = resource.name;
|
|
2654
|
-
const basePath = `/${version}/${resourceName}`;
|
|
2655
|
-
customMiddleware.forEach((middleware) => {
|
|
2656
|
-
app.use("*", middleware);
|
|
2657
|
-
});
|
|
2658
|
-
if (methods.includes("GET")) {
|
|
2659
|
-
app.get("/", asyncHandler(async (c) => {
|
|
2660
|
-
const query = c.req.query();
|
|
2661
|
-
const limit = parseInt(query.limit) || 100;
|
|
2662
|
-
const offset = parseInt(query.offset) || 0;
|
|
2663
|
-
const partition = query.partition;
|
|
2664
|
-
const partitionValues = query.partitionValues ? JSON.parse(query.partitionValues) : void 0;
|
|
2665
|
-
const reservedKeys = ["limit", "offset", "partition", "partitionValues", "sort"];
|
|
2666
|
-
const filters = {};
|
|
2667
|
-
for (const [key, value] of Object.entries(query)) {
|
|
2668
|
-
if (!reservedKeys.includes(key)) {
|
|
2669
|
-
try {
|
|
2670
|
-
filters[key] = JSON.parse(value);
|
|
2671
|
-
} catch {
|
|
2672
|
-
filters[key] = value;
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
let items;
|
|
2677
|
-
let total;
|
|
2678
|
-
if (Object.keys(filters).length > 0) {
|
|
2679
|
-
items = await resource.query(filters, { limit: limit + offset });
|
|
2680
|
-
items = items.slice(offset, offset + limit);
|
|
2681
|
-
total = items.length;
|
|
2682
|
-
} else if (partition && partitionValues) {
|
|
2683
|
-
items = await resource.listPartition({
|
|
2684
|
-
partition,
|
|
2685
|
-
partitionValues,
|
|
2686
|
-
limit: limit + offset
|
|
2687
|
-
});
|
|
2688
|
-
items = items.slice(offset, offset + limit);
|
|
2689
|
-
total = items.length;
|
|
2690
|
-
} else {
|
|
2691
|
-
items = await resource.list({ limit: limit + offset });
|
|
2692
|
-
items = items.slice(offset, offset + limit);
|
|
2693
|
-
total = items.length;
|
|
2694
|
-
}
|
|
2695
|
-
const response = list(items, {
|
|
2696
|
-
total,
|
|
2697
|
-
page: Math.floor(offset / limit) + 1,
|
|
2698
|
-
pageSize: limit,
|
|
2699
|
-
pageCount: Math.ceil(total / limit)
|
|
2700
|
-
});
|
|
2701
|
-
c.header("X-Total-Count", total.toString());
|
|
2702
|
-
c.header("X-Page-Count", Math.ceil(total / limit).toString());
|
|
2703
|
-
return c.json(response, response._status);
|
|
2704
|
-
}));
|
|
2705
|
-
}
|
|
2706
|
-
if (methods.includes("GET")) {
|
|
2707
|
-
app.get("/:id", asyncHandler(async (c) => {
|
|
2708
|
-
const id = c.req.param("id");
|
|
2709
|
-
const query = c.req.query();
|
|
2710
|
-
const partition = query.partition;
|
|
2711
|
-
const partitionValues = query.partitionValues ? JSON.parse(query.partitionValues) : void 0;
|
|
2712
|
-
let item;
|
|
2713
|
-
if (partition && partitionValues) {
|
|
2714
|
-
item = await resource.getFromPartition({
|
|
2715
|
-
id,
|
|
2716
|
-
partitionName: partition,
|
|
2717
|
-
partitionValues
|
|
2718
|
-
});
|
|
2719
|
-
} else {
|
|
2720
|
-
item = await resource.get(id);
|
|
2721
|
-
}
|
|
2722
|
-
if (!item) {
|
|
2723
|
-
const response2 = notFound(resourceName, id);
|
|
2724
|
-
return c.json(response2, response2._status);
|
|
2725
|
-
}
|
|
2726
|
-
const response = success(item);
|
|
2727
|
-
return c.json(response, response._status);
|
|
2728
|
-
}));
|
|
2729
|
-
}
|
|
2730
|
-
if (methods.includes("POST")) {
|
|
2731
|
-
app.post("/", asyncHandler(async (c) => {
|
|
2732
|
-
const data = await c.req.json();
|
|
2733
|
-
const item = await resource.insert(data);
|
|
2734
|
-
const location = `${basePath}/${item.id}`;
|
|
2735
|
-
const response = created(item, location);
|
|
2736
|
-
c.header("Location", location);
|
|
2737
|
-
return c.json(response, response._status);
|
|
2738
|
-
}));
|
|
2739
|
-
}
|
|
2740
|
-
if (methods.includes("PUT")) {
|
|
2741
|
-
app.put("/:id", asyncHandler(async (c) => {
|
|
2742
|
-
const id = c.req.param("id");
|
|
2743
|
-
const data = await c.req.json();
|
|
2744
|
-
const existing = await resource.get(id);
|
|
2745
|
-
if (!existing) {
|
|
2746
|
-
const response2 = notFound(resourceName, id);
|
|
2747
|
-
return c.json(response2, response2._status);
|
|
2748
|
-
}
|
|
2749
|
-
const updated = await resource.update(id, data);
|
|
2750
|
-
const response = success(updated);
|
|
2751
|
-
return c.json(response, response._status);
|
|
2752
|
-
}));
|
|
2753
|
-
}
|
|
2754
|
-
if (methods.includes("PATCH")) {
|
|
2755
|
-
app.patch("/:id", asyncHandler(async (c) => {
|
|
2756
|
-
const id = c.req.param("id");
|
|
2757
|
-
const data = await c.req.json();
|
|
2758
|
-
const existing = await resource.get(id);
|
|
2759
|
-
if (!existing) {
|
|
2760
|
-
const response2 = notFound(resourceName, id);
|
|
2761
|
-
return c.json(response2, response2._status);
|
|
2762
|
-
}
|
|
2763
|
-
const merged = { ...existing, ...data, id };
|
|
2764
|
-
const updated = await resource.update(id, merged);
|
|
2765
|
-
const response = success(updated);
|
|
2766
|
-
return c.json(response, response._status);
|
|
2767
|
-
}));
|
|
2768
|
-
}
|
|
2769
|
-
if (methods.includes("DELETE")) {
|
|
2770
|
-
app.delete("/:id", asyncHandler(async (c) => {
|
|
2771
|
-
const id = c.req.param("id");
|
|
2772
|
-
const existing = await resource.get(id);
|
|
2773
|
-
if (!existing) {
|
|
2774
|
-
const response2 = notFound(resourceName, id);
|
|
2775
|
-
return c.json(response2, response2._status);
|
|
2776
|
-
}
|
|
2777
|
-
await resource.delete(id);
|
|
2778
|
-
const response = noContent();
|
|
2779
|
-
return c.json(response, response._status);
|
|
2780
|
-
}));
|
|
2781
|
-
}
|
|
2782
|
-
if (methods.includes("HEAD")) {
|
|
2783
|
-
app.on("HEAD", "/", asyncHandler(async (c) => {
|
|
2784
|
-
const total = await resource.count();
|
|
2785
|
-
const allItems = await resource.list({ limit: 1e3 });
|
|
2786
|
-
const stats = {
|
|
2787
|
-
total,
|
|
2788
|
-
version: resource.config?.currentVersion || resource.version || "v1"
|
|
2789
|
-
};
|
|
2790
|
-
c.header("X-Total-Count", total.toString());
|
|
2791
|
-
c.header("X-Resource-Version", stats.version);
|
|
2792
|
-
c.header("X-Schema-Fields", Object.keys(resource.config?.attributes || {}).length.toString());
|
|
2793
|
-
return c.body(null, 200);
|
|
2794
|
-
}));
|
|
2795
|
-
app.on("HEAD", "/:id", asyncHandler(async (c) => {
|
|
2796
|
-
const id = c.req.param("id");
|
|
2797
|
-
const item = await resource.get(id);
|
|
2798
|
-
if (!item) {
|
|
2799
|
-
return c.body(null, 404);
|
|
2800
|
-
}
|
|
2801
|
-
if (item.updatedAt) {
|
|
2802
|
-
c.header("Last-Modified", new Date(item.updatedAt).toUTCString());
|
|
2803
|
-
}
|
|
2804
|
-
return c.body(null, 200);
|
|
2805
|
-
}));
|
|
2806
|
-
}
|
|
2807
|
-
if (methods.includes("OPTIONS")) {
|
|
2808
|
-
app.options("/", asyncHandler(async (c) => {
|
|
2809
|
-
c.header("Allow", methods.join(", "));
|
|
2810
|
-
const total = await resource.count();
|
|
2811
|
-
const schema = resource.config?.attributes || {};
|
|
2812
|
-
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
2813
|
-
const metadata = {
|
|
2814
|
-
resource: resourceName,
|
|
2815
|
-
version: version2,
|
|
2816
|
-
totalRecords: total,
|
|
2817
|
-
allowedMethods: methods,
|
|
2818
|
-
schema: Object.entries(schema).map(([name, def]) => ({
|
|
2819
|
-
name,
|
|
2820
|
-
type: typeof def === "string" ? def.split("|")[0] : def.type,
|
|
2821
|
-
rules: typeof def === "string" ? def.split("|").slice(1) : []
|
|
2822
|
-
})),
|
|
2823
|
-
endpoints: {
|
|
2824
|
-
list: `/${version2}/${resourceName}`,
|
|
2825
|
-
get: `/${version2}/${resourceName}/:id`,
|
|
2826
|
-
create: `/${version2}/${resourceName}`,
|
|
2827
|
-
update: `/${version2}/${resourceName}/:id`,
|
|
2828
|
-
delete: `/${version2}/${resourceName}/:id`
|
|
2829
|
-
},
|
|
2830
|
-
queryParameters: {
|
|
2831
|
-
limit: "number (1-1000, default: 100)",
|
|
2832
|
-
offset: "number (min: 0, default: 0)",
|
|
2833
|
-
partition: "string (partition name)",
|
|
2834
|
-
partitionValues: "JSON string",
|
|
2835
|
-
"[any field]": "any (filter by field value)"
|
|
2836
|
-
}
|
|
2837
|
-
};
|
|
2838
|
-
return c.json(metadata);
|
|
2839
|
-
}));
|
|
2840
|
-
app.options("/:id", (c) => {
|
|
2841
|
-
c.header("Allow", methods.filter((m) => m !== "POST").join(", "));
|
|
2842
|
-
return c.body(null, 204);
|
|
2843
|
-
});
|
|
2844
|
-
}
|
|
2845
|
-
return app;
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
function mapFieldTypeToOpenAPI(fieldType) {
|
|
2849
|
-
const type = fieldType.split("|")[0].trim();
|
|
2850
|
-
const typeMap = {
|
|
2851
|
-
"string": { type: "string" },
|
|
2852
|
-
"number": { type: "number" },
|
|
2853
|
-
"integer": { type: "integer" },
|
|
2854
|
-
"boolean": { type: "boolean" },
|
|
2855
|
-
"array": { type: "array", items: { type: "string" } },
|
|
2856
|
-
"object": { type: "object" },
|
|
2857
|
-
"json": { type: "object" },
|
|
2858
|
-
"secret": { type: "string", format: "password" },
|
|
2859
|
-
"email": { type: "string", format: "email" },
|
|
2860
|
-
"url": { type: "string", format: "uri" },
|
|
2861
|
-
"date": { type: "string", format: "date" },
|
|
2862
|
-
"datetime": { type: "string", format: "date-time" },
|
|
2863
|
-
"ip4": { type: "string", format: "ipv4", description: "IPv4 address" },
|
|
2864
|
-
"ip6": { type: "string", format: "ipv6", description: "IPv6 address" },
|
|
2865
|
-
"embedding": { type: "array", items: { type: "number" }, description: "Vector embedding" }
|
|
2866
|
-
};
|
|
2867
|
-
if (type.startsWith("embedding:")) {
|
|
2868
|
-
const length = parseInt(type.split(":")[1]);
|
|
2869
|
-
return {
|
|
2870
|
-
type: "array",
|
|
2871
|
-
items: { type: "number" },
|
|
2872
|
-
minItems: length,
|
|
2873
|
-
maxItems: length,
|
|
2874
|
-
description: `Vector embedding (${length} dimensions)`
|
|
2875
|
-
};
|
|
2876
|
-
}
|
|
2877
|
-
return typeMap[type] || { type: "string" };
|
|
2878
|
-
}
|
|
2879
|
-
function extractValidationRules(fieldDef) {
|
|
2880
|
-
const rules = {};
|
|
2881
|
-
const parts = fieldDef.split("|");
|
|
2882
|
-
for (const part of parts) {
|
|
2883
|
-
const [rule, value] = part.split(":").map((s) => s.trim());
|
|
2884
|
-
switch (rule) {
|
|
2885
|
-
case "required":
|
|
2886
|
-
rules.required = true;
|
|
2887
|
-
break;
|
|
2888
|
-
case "min":
|
|
2889
|
-
rules.minimum = parseFloat(value);
|
|
2890
|
-
break;
|
|
2891
|
-
case "max":
|
|
2892
|
-
rules.maximum = parseFloat(value);
|
|
2893
|
-
break;
|
|
2894
|
-
case "minlength":
|
|
2895
|
-
rules.minLength = parseInt(value);
|
|
2896
|
-
break;
|
|
2897
|
-
case "maxlength":
|
|
2898
|
-
rules.maxLength = parseInt(value);
|
|
2899
|
-
break;
|
|
2900
|
-
case "pattern":
|
|
2901
|
-
rules.pattern = value;
|
|
2902
|
-
break;
|
|
2903
|
-
case "enum":
|
|
2904
|
-
rules.enum = value.split(",").map((v) => v.trim());
|
|
2905
|
-
break;
|
|
2906
|
-
case "default":
|
|
2907
|
-
rules.default = value;
|
|
2908
|
-
break;
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
return rules;
|
|
2912
|
-
}
|
|
2913
|
-
function generateResourceSchema(resource) {
|
|
2914
|
-
const properties = {};
|
|
2915
|
-
const required = [];
|
|
2916
|
-
const attributes = resource.config?.attributes || resource.attributes || {};
|
|
2917
|
-
properties.id = {
|
|
2918
|
-
type: "string",
|
|
2919
|
-
description: "Unique identifier for the resource",
|
|
2920
|
-
example: "2_gDTpeU6EI0e8B92n_R3Y",
|
|
2921
|
-
readOnly: true
|
|
2922
|
-
};
|
|
2923
|
-
for (const [fieldName, fieldDef] of Object.entries(attributes)) {
|
|
2924
|
-
if (typeof fieldDef === "object" && fieldDef.type) {
|
|
2925
|
-
const baseType = mapFieldTypeToOpenAPI(fieldDef.type);
|
|
2926
|
-
properties[fieldName] = {
|
|
2927
|
-
...baseType,
|
|
2928
|
-
description: fieldDef.description || void 0
|
|
2929
|
-
};
|
|
2930
|
-
if (fieldDef.required) {
|
|
2931
|
-
required.push(fieldName);
|
|
2932
|
-
}
|
|
2933
|
-
if (fieldDef.type === "object" && fieldDef.props) {
|
|
2934
|
-
properties[fieldName].properties = {};
|
|
2935
|
-
for (const [propName, propDef] of Object.entries(fieldDef.props)) {
|
|
2936
|
-
const propType = typeof propDef === "string" ? propDef : propDef.type;
|
|
2937
|
-
properties[fieldName].properties[propName] = mapFieldTypeToOpenAPI(propType);
|
|
2938
|
-
}
|
|
2939
|
-
}
|
|
2940
|
-
if (fieldDef.type === "array" && fieldDef.items) {
|
|
2941
|
-
properties[fieldName].items = mapFieldTypeToOpenAPI(fieldDef.items);
|
|
2942
|
-
}
|
|
2943
|
-
} else if (typeof fieldDef === "string") {
|
|
2944
|
-
const baseType = mapFieldTypeToOpenAPI(fieldDef);
|
|
2945
|
-
const rules = extractValidationRules(fieldDef);
|
|
2946
|
-
properties[fieldName] = {
|
|
2947
|
-
...baseType,
|
|
2948
|
-
...rules
|
|
2949
|
-
};
|
|
2950
|
-
if (rules.required) {
|
|
2951
|
-
required.push(fieldName);
|
|
2952
|
-
delete properties[fieldName].required;
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
return {
|
|
2957
|
-
type: "object",
|
|
2958
|
-
properties,
|
|
2959
|
-
required: required.length > 0 ? required : void 0
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
function generateResourcePaths(resource, version, config = {}) {
|
|
2963
|
-
const resourceName = resource.name;
|
|
2964
|
-
const basePath = `/${version}/${resourceName}`;
|
|
2965
|
-
const schema = generateResourceSchema(resource);
|
|
2966
|
-
const methods = config.methods || ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
2967
|
-
const authMethods = config.auth || [];
|
|
2968
|
-
const requiresAuth = authMethods && authMethods.length > 0;
|
|
2969
|
-
const paths = {};
|
|
2970
|
-
const security = [];
|
|
2971
|
-
if (requiresAuth) {
|
|
2972
|
-
if (authMethods.includes("jwt")) security.push({ bearerAuth: [] });
|
|
2973
|
-
if (authMethods.includes("apiKey")) security.push({ apiKeyAuth: [] });
|
|
2974
|
-
if (authMethods.includes("basic")) security.push({ basicAuth: [] });
|
|
2975
|
-
}
|
|
2976
|
-
const partitions = resource.config?.options?.partitions || resource.config?.partitions || resource.partitions || {};
|
|
2977
|
-
const partitionNames = Object.keys(partitions);
|
|
2978
|
-
const hasPartitions = partitionNames.length > 0;
|
|
2979
|
-
let partitionDescription = "Partition name for filtering";
|
|
2980
|
-
let partitionValuesDescription = "Partition values as JSON string";
|
|
2981
|
-
let partitionExample = void 0;
|
|
2982
|
-
let partitionValuesExample = void 0;
|
|
2983
|
-
if (hasPartitions) {
|
|
2984
|
-
const partitionDocs = partitionNames.map((name) => {
|
|
2985
|
-
const partition = partitions[name];
|
|
2986
|
-
const fields = Object.keys(partition.fields || {});
|
|
2987
|
-
const fieldTypes = Object.entries(partition.fields || {}).map(([field, type]) => `${field}: ${type}`).join(", ");
|
|
2988
|
-
return `- **${name}**: Filters by ${fields.join(", ")} (${fieldTypes})`;
|
|
2989
|
-
}).join("\n");
|
|
2990
|
-
partitionDescription = `Available partitions:
|
|
2991
|
-
${partitionDocs}`;
|
|
2992
|
-
const examplePartition = partitionNames[0];
|
|
2993
|
-
const exampleFields = partitions[examplePartition]?.fields || {};
|
|
2994
|
-
Object.entries(exampleFields).map(([field, type]) => `"${field}": <${type} value>`).join(", ");
|
|
2995
|
-
partitionValuesDescription = `Partition field values as JSON string. Must match the structure of the selected partition.
|
|
2996
|
-
|
|
2997
|
-
Example for "${examplePartition}" partition: \`{"${Object.keys(exampleFields)[0]}": "value"}\``;
|
|
2998
|
-
partitionExample = examplePartition;
|
|
2999
|
-
const firstField = Object.keys(exampleFields)[0];
|
|
3000
|
-
const firstFieldType = exampleFields[firstField];
|
|
3001
|
-
let exampleValue = "example";
|
|
3002
|
-
if (firstFieldType === "number" || firstFieldType === "integer") {
|
|
3003
|
-
exampleValue = 123;
|
|
3004
|
-
} else if (firstFieldType === "boolean") {
|
|
3005
|
-
exampleValue = true;
|
|
3006
|
-
}
|
|
3007
|
-
partitionValuesExample = JSON.stringify({ [firstField]: exampleValue });
|
|
3008
|
-
}
|
|
3009
|
-
const attributeQueryParams = [];
|
|
3010
|
-
if (hasPartitions) {
|
|
3011
|
-
const partitionFieldsSet = /* @__PURE__ */ new Set();
|
|
3012
|
-
for (const [partitionName, partition] of Object.entries(partitions)) {
|
|
3013
|
-
const fields = partition.fields || {};
|
|
3014
|
-
for (const fieldName of Object.keys(fields)) {
|
|
3015
|
-
partitionFieldsSet.add(fieldName);
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
const attributes = resource.config?.attributes || resource.attributes || {};
|
|
3019
|
-
for (const fieldName of partitionFieldsSet) {
|
|
3020
|
-
const fieldDef = attributes[fieldName];
|
|
3021
|
-
if (!fieldDef) continue;
|
|
3022
|
-
let fieldType;
|
|
3023
|
-
if (typeof fieldDef === "object" && fieldDef.type) {
|
|
3024
|
-
fieldType = fieldDef.type;
|
|
3025
|
-
} else if (typeof fieldDef === "string") {
|
|
3026
|
-
fieldType = fieldDef.split("|")[0].trim();
|
|
3027
|
-
} else {
|
|
3028
|
-
fieldType = "string";
|
|
3029
|
-
}
|
|
3030
|
-
const openAPIType = mapFieldTypeToOpenAPI(fieldType);
|
|
3031
|
-
attributeQueryParams.push({
|
|
3032
|
-
name: fieldName,
|
|
3033
|
-
in: "query",
|
|
3034
|
-
description: `Filter by ${fieldName} field (indexed via partitions for efficient querying). Value will be parsed as JSON if possible, otherwise treated as string.`,
|
|
3035
|
-
required: false,
|
|
3036
|
-
schema: openAPIType
|
|
3037
|
-
});
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
if (methods.includes("GET")) {
|
|
3041
|
-
paths[basePath] = {
|
|
3042
|
-
get: {
|
|
3043
|
-
tags: [resourceName],
|
|
3044
|
-
summary: `List ${resourceName}`,
|
|
3045
|
-
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.
|
|
3046
|
-
|
|
3047
|
-
**Pagination**: Use \`limit\` and \`offset\` to paginate results. For example:
|
|
3048
|
-
- First page (10 items): \`?limit=10&offset=0\`
|
|
3049
|
-
- Second page: \`?limit=10&offset=10\`
|
|
3050
|
-
- Third page: \`?limit=10&offset=20\`
|
|
3051
|
-
|
|
3052
|
-
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." : ""}`,
|
|
3053
|
-
parameters: [
|
|
3054
|
-
{
|
|
3055
|
-
name: "limit",
|
|
3056
|
-
in: "query",
|
|
3057
|
-
description: "Maximum number of items to return per page (page size)",
|
|
3058
|
-
schema: { type: "integer", default: 100, minimum: 1, maximum: 1e3 },
|
|
3059
|
-
example: 10
|
|
3060
|
-
},
|
|
3061
|
-
{
|
|
3062
|
-
name: "offset",
|
|
3063
|
-
in: "query",
|
|
3064
|
-
description: "Number of items to skip before starting to return results. Use for pagination: offset = (page - 1) * limit",
|
|
3065
|
-
schema: { type: "integer", default: 0, minimum: 0 },
|
|
3066
|
-
example: 0
|
|
3067
|
-
},
|
|
3068
|
-
...hasPartitions ? [
|
|
3069
|
-
{
|
|
3070
|
-
name: "partition",
|
|
3071
|
-
in: "query",
|
|
3072
|
-
description: partitionDescription,
|
|
3073
|
-
schema: {
|
|
3074
|
-
type: "string",
|
|
3075
|
-
enum: partitionNames
|
|
3076
|
-
},
|
|
3077
|
-
example: partitionExample
|
|
3078
|
-
},
|
|
3079
|
-
{
|
|
3080
|
-
name: "partitionValues",
|
|
3081
|
-
in: "query",
|
|
3082
|
-
description: partitionValuesDescription,
|
|
3083
|
-
schema: { type: "string" },
|
|
3084
|
-
example: partitionValuesExample
|
|
3085
|
-
}
|
|
3086
|
-
] : [],
|
|
3087
|
-
...attributeQueryParams
|
|
3088
|
-
],
|
|
3089
|
-
responses: {
|
|
3090
|
-
200: {
|
|
3091
|
-
description: "Successful response",
|
|
3092
|
-
content: {
|
|
3093
|
-
"application/json": {
|
|
3094
|
-
schema: {
|
|
3095
|
-
type: "object",
|
|
3096
|
-
properties: {
|
|
3097
|
-
success: { type: "boolean", example: true },
|
|
3098
|
-
data: {
|
|
3099
|
-
type: "array",
|
|
3100
|
-
items: schema
|
|
3101
|
-
},
|
|
3102
|
-
pagination: {
|
|
3103
|
-
type: "object",
|
|
3104
|
-
description: "Pagination metadata for the current request",
|
|
3105
|
-
properties: {
|
|
3106
|
-
total: {
|
|
3107
|
-
type: "integer",
|
|
3108
|
-
description: "Total number of items available",
|
|
3109
|
-
example: 150
|
|
3110
|
-
},
|
|
3111
|
-
page: {
|
|
3112
|
-
type: "integer",
|
|
3113
|
-
description: "Current page number (1-indexed)",
|
|
3114
|
-
example: 1
|
|
3115
|
-
},
|
|
3116
|
-
pageSize: {
|
|
3117
|
-
type: "integer",
|
|
3118
|
-
description: "Number of items per page (same as limit parameter)",
|
|
3119
|
-
example: 10
|
|
3120
|
-
},
|
|
3121
|
-
pageCount: {
|
|
3122
|
-
type: "integer",
|
|
3123
|
-
description: "Total number of pages available",
|
|
3124
|
-
example: 15
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
}
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
|
-
},
|
|
3132
|
-
headers: {
|
|
3133
|
-
"X-Total-Count": {
|
|
3134
|
-
description: "Total number of records",
|
|
3135
|
-
schema: { type: "integer" }
|
|
3136
|
-
},
|
|
3137
|
-
"X-Page-Count": {
|
|
3138
|
-
description: "Total number of pages",
|
|
3139
|
-
schema: { type: "integer" }
|
|
3140
|
-
}
|
|
3141
|
-
}
|
|
3142
|
-
}
|
|
3143
|
-
},
|
|
3144
|
-
security: security.length > 0 ? security : void 0
|
|
3145
|
-
}
|
|
3146
|
-
};
|
|
3147
|
-
}
|
|
3148
|
-
if (methods.includes("GET")) {
|
|
3149
|
-
paths[`${basePath}/{id}`] = {
|
|
3150
|
-
get: {
|
|
3151
|
-
tags: [resourceName],
|
|
3152
|
-
summary: `Get ${resourceName} by ID`,
|
|
3153
|
-
description: `Retrieve a single ${resourceName} by its ID${hasPartitions ? ". Optionally specify a partition for more efficient retrieval." : ""}`,
|
|
3154
|
-
parameters: [
|
|
3155
|
-
{
|
|
3156
|
-
name: "id",
|
|
3157
|
-
in: "path",
|
|
3158
|
-
required: true,
|
|
3159
|
-
description: `${resourceName} ID`,
|
|
3160
|
-
schema: { type: "string" }
|
|
3161
|
-
},
|
|
3162
|
-
...hasPartitions ? [
|
|
3163
|
-
{
|
|
3164
|
-
name: "partition",
|
|
3165
|
-
in: "query",
|
|
3166
|
-
description: partitionDescription,
|
|
3167
|
-
schema: {
|
|
3168
|
-
type: "string",
|
|
3169
|
-
enum: partitionNames
|
|
3170
|
-
},
|
|
3171
|
-
example: partitionExample
|
|
3172
|
-
},
|
|
3173
|
-
{
|
|
3174
|
-
name: "partitionValues",
|
|
3175
|
-
in: "query",
|
|
3176
|
-
description: partitionValuesDescription,
|
|
3177
|
-
schema: { type: "string" },
|
|
3178
|
-
example: partitionValuesExample
|
|
3179
|
-
}
|
|
3180
|
-
] : []
|
|
3181
|
-
],
|
|
3182
|
-
responses: {
|
|
3183
|
-
200: {
|
|
3184
|
-
description: "Successful response",
|
|
3185
|
-
content: {
|
|
3186
|
-
"application/json": {
|
|
3187
|
-
schema: {
|
|
3188
|
-
type: "object",
|
|
3189
|
-
properties: {
|
|
3190
|
-
success: { type: "boolean", example: true },
|
|
3191
|
-
data: schema
|
|
3192
|
-
}
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
},
|
|
3197
|
-
404: {
|
|
3198
|
-
description: "Resource not found",
|
|
3199
|
-
content: {
|
|
3200
|
-
"application/json": {
|
|
3201
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
},
|
|
3206
|
-
security: security.length > 0 ? security : void 0
|
|
3207
|
-
}
|
|
3208
|
-
};
|
|
3209
|
-
}
|
|
3210
|
-
if (methods.includes("POST")) {
|
|
3211
|
-
if (!paths[basePath]) paths[basePath] = {};
|
|
3212
|
-
paths[basePath].post = {
|
|
3213
|
-
tags: [resourceName],
|
|
3214
|
-
summary: `Create ${resourceName}`,
|
|
3215
|
-
description: `Create a new ${resourceName}`,
|
|
3216
|
-
requestBody: {
|
|
3217
|
-
required: true,
|
|
3218
|
-
content: {
|
|
3219
|
-
"application/json": {
|
|
3220
|
-
schema
|
|
3221
|
-
}
|
|
3222
|
-
}
|
|
3223
|
-
},
|
|
3224
|
-
responses: {
|
|
3225
|
-
201: {
|
|
3226
|
-
description: "Resource created successfully",
|
|
3227
|
-
content: {
|
|
3228
|
-
"application/json": {
|
|
3229
|
-
schema: {
|
|
3230
|
-
type: "object",
|
|
3231
|
-
properties: {
|
|
3232
|
-
success: { type: "boolean", example: true },
|
|
3233
|
-
data: schema
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
},
|
|
3238
|
-
headers: {
|
|
3239
|
-
Location: {
|
|
3240
|
-
description: "URL of the created resource",
|
|
3241
|
-
schema: { type: "string" }
|
|
3242
|
-
}
|
|
3243
|
-
}
|
|
3244
|
-
},
|
|
3245
|
-
400: {
|
|
3246
|
-
description: "Validation error",
|
|
3247
|
-
content: {
|
|
3248
|
-
"application/json": {
|
|
3249
|
-
schema: { $ref: "#/components/schemas/ValidationError" }
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
},
|
|
3254
|
-
security: security.length > 0 ? security : void 0
|
|
3255
|
-
};
|
|
3256
|
-
}
|
|
3257
|
-
if (methods.includes("PUT")) {
|
|
3258
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3259
|
-
paths[`${basePath}/{id}`].put = {
|
|
3260
|
-
tags: [resourceName],
|
|
3261
|
-
summary: `Update ${resourceName} (full)`,
|
|
3262
|
-
description: `Fully update a ${resourceName}`,
|
|
3263
|
-
parameters: [
|
|
3264
|
-
{
|
|
3265
|
-
name: "id",
|
|
3266
|
-
in: "path",
|
|
3267
|
-
required: true,
|
|
3268
|
-
schema: { type: "string" }
|
|
3269
|
-
}
|
|
3270
|
-
],
|
|
3271
|
-
requestBody: {
|
|
3272
|
-
required: true,
|
|
3273
|
-
content: {
|
|
3274
|
-
"application/json": {
|
|
3275
|
-
schema
|
|
3276
|
-
}
|
|
3277
|
-
}
|
|
3278
|
-
},
|
|
3279
|
-
responses: {
|
|
3280
|
-
200: {
|
|
3281
|
-
description: "Resource updated successfully",
|
|
3282
|
-
content: {
|
|
3283
|
-
"application/json": {
|
|
3284
|
-
schema: {
|
|
3285
|
-
type: "object",
|
|
3286
|
-
properties: {
|
|
3287
|
-
success: { type: "boolean", example: true },
|
|
3288
|
-
data: schema
|
|
3289
|
-
}
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
}
|
|
3293
|
-
},
|
|
3294
|
-
404: {
|
|
3295
|
-
description: "Resource not found",
|
|
3296
|
-
content: {
|
|
3297
|
-
"application/json": {
|
|
3298
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3299
|
-
}
|
|
3300
|
-
}
|
|
3301
|
-
}
|
|
3302
|
-
},
|
|
3303
|
-
security: security.length > 0 ? security : void 0
|
|
3304
|
-
};
|
|
3305
|
-
}
|
|
3306
|
-
if (methods.includes("PATCH")) {
|
|
3307
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3308
|
-
paths[`${basePath}/{id}`].patch = {
|
|
3309
|
-
tags: [resourceName],
|
|
3310
|
-
summary: `Update ${resourceName} (partial)`,
|
|
3311
|
-
description: `Partially update a ${resourceName}`,
|
|
3312
|
-
parameters: [
|
|
3313
|
-
{
|
|
3314
|
-
name: "id",
|
|
3315
|
-
in: "path",
|
|
3316
|
-
required: true,
|
|
3317
|
-
schema: { type: "string" }
|
|
3318
|
-
}
|
|
3319
|
-
],
|
|
3320
|
-
requestBody: {
|
|
3321
|
-
required: true,
|
|
3322
|
-
content: {
|
|
3323
|
-
"application/json": {
|
|
3324
|
-
schema: {
|
|
3325
|
-
...schema,
|
|
3326
|
-
required: void 0
|
|
3327
|
-
// Partial updates don't require all fields
|
|
3328
|
-
}
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
},
|
|
3332
|
-
responses: {
|
|
3333
|
-
200: {
|
|
3334
|
-
description: "Resource updated successfully",
|
|
3335
|
-
content: {
|
|
3336
|
-
"application/json": {
|
|
3337
|
-
schema: {
|
|
3338
|
-
type: "object",
|
|
3339
|
-
properties: {
|
|
3340
|
-
success: { type: "boolean", example: true },
|
|
3341
|
-
data: schema
|
|
3342
|
-
}
|
|
3343
|
-
}
|
|
3344
|
-
}
|
|
3345
|
-
}
|
|
3346
|
-
},
|
|
3347
|
-
404: {
|
|
3348
|
-
description: "Resource not found",
|
|
3349
|
-
content: {
|
|
3350
|
-
"application/json": {
|
|
3351
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
}
|
|
3355
|
-
},
|
|
3356
|
-
security: security.length > 0 ? security : void 0
|
|
3357
|
-
};
|
|
3358
|
-
}
|
|
3359
|
-
if (methods.includes("DELETE")) {
|
|
3360
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3361
|
-
paths[`${basePath}/{id}`].delete = {
|
|
3362
|
-
tags: [resourceName],
|
|
3363
|
-
summary: `Delete ${resourceName}`,
|
|
3364
|
-
description: `Delete a ${resourceName} by ID`,
|
|
3365
|
-
parameters: [
|
|
3366
|
-
{
|
|
3367
|
-
name: "id",
|
|
3368
|
-
in: "path",
|
|
3369
|
-
required: true,
|
|
3370
|
-
schema: { type: "string" }
|
|
3371
|
-
}
|
|
3372
|
-
],
|
|
3373
|
-
responses: {
|
|
3374
|
-
204: {
|
|
3375
|
-
description: "Resource deleted successfully"
|
|
3376
|
-
},
|
|
3377
|
-
404: {
|
|
3378
|
-
description: "Resource not found",
|
|
3379
|
-
content: {
|
|
3380
|
-
"application/json": {
|
|
3381
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
}
|
|
3385
|
-
},
|
|
3386
|
-
security: security.length > 0 ? security : void 0
|
|
3387
|
-
};
|
|
3388
|
-
}
|
|
3389
|
-
if (methods.includes("HEAD")) {
|
|
3390
|
-
if (!paths[basePath]) paths[basePath] = {};
|
|
3391
|
-
paths[basePath].head = {
|
|
3392
|
-
tags: [resourceName],
|
|
3393
|
-
summary: `Get ${resourceName} statistics`,
|
|
3394
|
-
description: `Get statistics about ${resourceName} collection without retrieving data. Returns statistics in response headers.`,
|
|
3395
|
-
responses: {
|
|
3396
|
-
200: {
|
|
3397
|
-
description: "Statistics retrieved successfully",
|
|
3398
|
-
headers: {
|
|
3399
|
-
"X-Total-Count": {
|
|
3400
|
-
description: "Total number of records",
|
|
3401
|
-
schema: { type: "integer" }
|
|
3402
|
-
},
|
|
3403
|
-
"X-Resource-Version": {
|
|
3404
|
-
description: "Current resource version",
|
|
3405
|
-
schema: { type: "string" }
|
|
3406
|
-
},
|
|
3407
|
-
"X-Schema-Fields": {
|
|
3408
|
-
description: "Number of schema fields",
|
|
3409
|
-
schema: { type: "integer" }
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
},
|
|
3414
|
-
security: security.length > 0 ? security : void 0
|
|
3415
|
-
};
|
|
3416
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3417
|
-
paths[`${basePath}/{id}`].head = {
|
|
3418
|
-
tags: [resourceName],
|
|
3419
|
-
summary: `Check if ${resourceName} exists`,
|
|
3420
|
-
description: `Check if a ${resourceName} exists without retrieving its data`,
|
|
3421
|
-
parameters: [
|
|
3422
|
-
{
|
|
3423
|
-
name: "id",
|
|
3424
|
-
in: "path",
|
|
3425
|
-
required: true,
|
|
3426
|
-
schema: { type: "string" }
|
|
3427
|
-
}
|
|
3428
|
-
],
|
|
3429
|
-
responses: {
|
|
3430
|
-
200: {
|
|
3431
|
-
description: "Resource exists",
|
|
3432
|
-
headers: {
|
|
3433
|
-
"Last-Modified": {
|
|
3434
|
-
description: "Last modification date",
|
|
3435
|
-
schema: { type: "string", format: "date-time" }
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
},
|
|
3439
|
-
404: {
|
|
3440
|
-
description: "Resource not found"
|
|
3441
|
-
}
|
|
3442
|
-
},
|
|
3443
|
-
security: security.length > 0 ? security : void 0
|
|
3444
|
-
};
|
|
3445
|
-
}
|
|
3446
|
-
if (methods.includes("OPTIONS")) {
|
|
3447
|
-
if (!paths[basePath]) paths[basePath] = {};
|
|
3448
|
-
paths[basePath].options = {
|
|
3449
|
-
tags: [resourceName],
|
|
3450
|
-
summary: `Get ${resourceName} metadata`,
|
|
3451
|
-
description: `Get complete metadata about ${resourceName} resource including schema, allowed methods, endpoints, and query parameters`,
|
|
3452
|
-
responses: {
|
|
3453
|
-
200: {
|
|
3454
|
-
description: "Metadata retrieved successfully",
|
|
3455
|
-
headers: {
|
|
3456
|
-
"Allow": {
|
|
3457
|
-
description: "Allowed HTTP methods",
|
|
3458
|
-
schema: { type: "string", example: "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS" }
|
|
3459
|
-
}
|
|
3460
|
-
},
|
|
3461
|
-
content: {
|
|
3462
|
-
"application/json": {
|
|
3463
|
-
schema: {
|
|
3464
|
-
type: "object",
|
|
3465
|
-
properties: {
|
|
3466
|
-
resource: { type: "string" },
|
|
3467
|
-
version: { type: "string" },
|
|
3468
|
-
totalRecords: { type: "integer" },
|
|
3469
|
-
allowedMethods: {
|
|
3470
|
-
type: "array",
|
|
3471
|
-
items: { type: "string" }
|
|
3472
|
-
},
|
|
3473
|
-
schema: {
|
|
3474
|
-
type: "array",
|
|
3475
|
-
items: {
|
|
3476
|
-
type: "object",
|
|
3477
|
-
properties: {
|
|
3478
|
-
name: { type: "string" },
|
|
3479
|
-
type: { type: "string" },
|
|
3480
|
-
rules: { type: "array", items: { type: "string" } }
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
},
|
|
3484
|
-
endpoints: {
|
|
3485
|
-
type: "object",
|
|
3486
|
-
properties: {
|
|
3487
|
-
list: { type: "string" },
|
|
3488
|
-
get: { type: "string" },
|
|
3489
|
-
create: { type: "string" },
|
|
3490
|
-
update: { type: "string" },
|
|
3491
|
-
delete: { type: "string" }
|
|
3492
|
-
}
|
|
3493
|
-
},
|
|
3494
|
-
queryParameters: { type: "object" }
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
}
|
|
3500
|
-
}
|
|
3501
|
-
};
|
|
3502
|
-
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3503
|
-
paths[`${basePath}/{id}`].options = {
|
|
3504
|
-
tags: [resourceName],
|
|
3505
|
-
summary: `Get allowed methods for ${resourceName} item`,
|
|
3506
|
-
description: `Get allowed HTTP methods for individual ${resourceName} operations`,
|
|
3507
|
-
parameters: [
|
|
3508
|
-
{
|
|
3509
|
-
name: "id",
|
|
3510
|
-
in: "path",
|
|
3511
|
-
required: true,
|
|
3512
|
-
schema: { type: "string" }
|
|
3513
|
-
}
|
|
3514
|
-
],
|
|
3515
|
-
responses: {
|
|
3516
|
-
204: {
|
|
3517
|
-
description: "Methods retrieved successfully",
|
|
3518
|
-
headers: {
|
|
3519
|
-
"Allow": {
|
|
3520
|
-
description: "Allowed HTTP methods",
|
|
3521
|
-
schema: { type: "string", example: "GET, PUT, PATCH, DELETE, HEAD, OPTIONS" }
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
}
|
|
3526
|
-
};
|
|
3527
|
-
}
|
|
3528
|
-
return paths;
|
|
3529
|
-
}
|
|
3530
|
-
function generateOpenAPISpec(database, config = {}) {
|
|
3531
|
-
const {
|
|
3532
|
-
title = "s3db.js API",
|
|
3533
|
-
version = "1.0.0",
|
|
3534
|
-
description = "Auto-generated REST API documentation for s3db.js resources",
|
|
3535
|
-
serverUrl = "http://localhost:3000",
|
|
3536
|
-
auth = {},
|
|
3537
|
-
resources: resourceConfigs = {}
|
|
3538
|
-
} = config;
|
|
3539
|
-
const spec = {
|
|
3540
|
-
openapi: "3.1.0",
|
|
3541
|
-
info: {
|
|
3542
|
-
title,
|
|
3543
|
-
version,
|
|
3544
|
-
description,
|
|
3545
|
-
contact: {
|
|
3546
|
-
name: "s3db.js",
|
|
3547
|
-
url: "https://github.com/forattini-dev/s3db.js"
|
|
3548
|
-
}
|
|
3549
|
-
},
|
|
3550
|
-
servers: [
|
|
3551
|
-
{
|
|
3552
|
-
url: serverUrl,
|
|
3553
|
-
description: "API Server"
|
|
3554
|
-
}
|
|
3555
|
-
],
|
|
3556
|
-
paths: {},
|
|
3557
|
-
components: {
|
|
3558
|
-
schemas: {
|
|
3559
|
-
Error: {
|
|
3560
|
-
type: "object",
|
|
3561
|
-
properties: {
|
|
3562
|
-
success: { type: "boolean", example: false },
|
|
3563
|
-
error: {
|
|
3564
|
-
type: "object",
|
|
3565
|
-
properties: {
|
|
3566
|
-
message: { type: "string" },
|
|
3567
|
-
code: { type: "string" },
|
|
3568
|
-
details: { type: "object" }
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
},
|
|
3573
|
-
ValidationError: {
|
|
3574
|
-
type: "object",
|
|
3575
|
-
properties: {
|
|
3576
|
-
success: { type: "boolean", example: false },
|
|
3577
|
-
error: {
|
|
3578
|
-
type: "object",
|
|
3579
|
-
properties: {
|
|
3580
|
-
message: { type: "string", example: "Validation failed" },
|
|
3581
|
-
code: { type: "string", example: "VALIDATION_ERROR" },
|
|
3582
|
-
details: {
|
|
3583
|
-
type: "object",
|
|
3584
|
-
properties: {
|
|
3585
|
-
errors: {
|
|
3586
|
-
type: "array",
|
|
3587
|
-
items: {
|
|
3588
|
-
type: "object",
|
|
3589
|
-
properties: {
|
|
3590
|
-
field: { type: "string" },
|
|
3591
|
-
message: { type: "string" },
|
|
3592
|
-
expected: { type: "string" },
|
|
3593
|
-
actual: {}
|
|
3594
|
-
}
|
|
3595
|
-
}
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
|
-
}
|
|
3599
|
-
}
|
|
3600
|
-
}
|
|
3601
|
-
}
|
|
3602
|
-
}
|
|
3603
|
-
},
|
|
3604
|
-
securitySchemes: {}
|
|
3605
|
-
},
|
|
3606
|
-
tags: []
|
|
3607
|
-
};
|
|
3608
|
-
if (auth.jwt?.enabled) {
|
|
3609
|
-
spec.components.securitySchemes.bearerAuth = {
|
|
3610
|
-
type: "http",
|
|
3611
|
-
scheme: "bearer",
|
|
3612
|
-
bearerFormat: "JWT",
|
|
3613
|
-
description: "JWT authentication"
|
|
3614
|
-
};
|
|
3615
|
-
}
|
|
3616
|
-
if (auth.apiKey?.enabled) {
|
|
3617
|
-
spec.components.securitySchemes.apiKeyAuth = {
|
|
3618
|
-
type: "apiKey",
|
|
3619
|
-
in: "header",
|
|
3620
|
-
name: auth.apiKey.headerName || "X-API-Key",
|
|
3621
|
-
description: "API Key authentication"
|
|
3622
|
-
};
|
|
3623
|
-
}
|
|
3624
|
-
if (auth.basic?.enabled) {
|
|
3625
|
-
spec.components.securitySchemes.basicAuth = {
|
|
3626
|
-
type: "http",
|
|
3627
|
-
scheme: "basic",
|
|
3628
|
-
description: "HTTP Basic authentication"
|
|
3629
|
-
};
|
|
3630
|
-
}
|
|
3631
|
-
const resources = database.resources;
|
|
3632
|
-
for (const [name, resource] of Object.entries(resources)) {
|
|
3633
|
-
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
3634
|
-
continue;
|
|
3635
|
-
}
|
|
3636
|
-
const config2 = resourceConfigs[name] || {
|
|
3637
|
-
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
3638
|
-
auth: false
|
|
3639
|
-
};
|
|
3640
|
-
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
3641
|
-
const paths = generateResourcePaths(resource, version2, config2);
|
|
3642
|
-
Object.assign(spec.paths, paths);
|
|
3643
|
-
spec.tags.push({
|
|
3644
|
-
name,
|
|
3645
|
-
description: `Operations for ${name} resource`
|
|
3646
|
-
});
|
|
3647
|
-
spec.components.schemas[name] = generateResourceSchema(resource);
|
|
3648
|
-
}
|
|
3649
|
-
if (auth.jwt?.enabled || auth.apiKey?.enabled || auth.basic?.enabled) {
|
|
3650
|
-
spec.paths["/auth/login"] = {
|
|
3651
|
-
post: {
|
|
3652
|
-
tags: ["Authentication"],
|
|
3653
|
-
summary: "Login",
|
|
3654
|
-
description: "Authenticate with username and password",
|
|
3655
|
-
requestBody: {
|
|
3656
|
-
required: true,
|
|
3657
|
-
content: {
|
|
3658
|
-
"application/json": {
|
|
3659
|
-
schema: {
|
|
3660
|
-
type: "object",
|
|
3661
|
-
properties: {
|
|
3662
|
-
username: { type: "string" },
|
|
3663
|
-
password: { type: "string", format: "password" }
|
|
3664
|
-
},
|
|
3665
|
-
required: ["username", "password"]
|
|
3666
|
-
}
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
},
|
|
3670
|
-
responses: {
|
|
3671
|
-
200: {
|
|
3672
|
-
description: "Login successful",
|
|
3673
|
-
content: {
|
|
3674
|
-
"application/json": {
|
|
3675
|
-
schema: {
|
|
3676
|
-
type: "object",
|
|
3677
|
-
properties: {
|
|
3678
|
-
success: { type: "boolean", example: true },
|
|
3679
|
-
data: {
|
|
3680
|
-
type: "object",
|
|
3681
|
-
properties: {
|
|
3682
|
-
token: { type: "string" },
|
|
3683
|
-
user: { type: "object" }
|
|
3684
|
-
}
|
|
3685
|
-
}
|
|
3686
|
-
}
|
|
3687
|
-
}
|
|
3688
|
-
}
|
|
3689
|
-
}
|
|
3690
|
-
},
|
|
3691
|
-
401: {
|
|
3692
|
-
description: "Invalid credentials",
|
|
3693
|
-
content: {
|
|
3694
|
-
"application/json": {
|
|
3695
|
-
schema: { $ref: "#/components/schemas/Error" }
|
|
3696
|
-
}
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
}
|
|
3701
|
-
};
|
|
3702
|
-
spec.paths["/auth/register"] = {
|
|
3703
|
-
post: {
|
|
3704
|
-
tags: ["Authentication"],
|
|
3705
|
-
summary: "Register",
|
|
3706
|
-
description: "Register a new user",
|
|
3707
|
-
requestBody: {
|
|
3708
|
-
required: true,
|
|
3709
|
-
content: {
|
|
3710
|
-
"application/json": {
|
|
3711
|
-
schema: {
|
|
3712
|
-
type: "object",
|
|
3713
|
-
properties: {
|
|
3714
|
-
username: { type: "string", minLength: 3 },
|
|
3715
|
-
password: { type: "string", format: "password", minLength: 8 },
|
|
3716
|
-
email: { type: "string", format: "email" }
|
|
3717
|
-
},
|
|
3718
|
-
required: ["username", "password"]
|
|
3719
|
-
}
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
},
|
|
3723
|
-
responses: {
|
|
3724
|
-
201: {
|
|
3725
|
-
description: "User registered successfully",
|
|
3726
|
-
content: {
|
|
3727
|
-
"application/json": {
|
|
3728
|
-
schema: {
|
|
3729
|
-
type: "object",
|
|
3730
|
-
properties: {
|
|
3731
|
-
success: { type: "boolean", example: true },
|
|
3732
|
-
data: {
|
|
3733
|
-
type: "object",
|
|
3734
|
-
properties: {
|
|
3735
|
-
token: { type: "string" },
|
|
3736
|
-
user: { type: "object" }
|
|
3737
|
-
}
|
|
3738
|
-
}
|
|
3739
|
-
}
|
|
3740
|
-
}
|
|
3741
|
-
}
|
|
3742
|
-
}
|
|
3743
|
-
}
|
|
3744
|
-
}
|
|
3745
|
-
}
|
|
3746
|
-
};
|
|
3747
|
-
spec.tags.push({
|
|
3748
|
-
name: "Authentication",
|
|
3749
|
-
description: "Authentication endpoints"
|
|
3750
|
-
});
|
|
3751
|
-
}
|
|
3752
|
-
spec.paths["/health"] = {
|
|
3753
|
-
get: {
|
|
3754
|
-
tags: ["Health"],
|
|
3755
|
-
summary: "Generic Health Check",
|
|
3756
|
-
description: "Generic health check endpoint that includes references to liveness and readiness probes",
|
|
3757
|
-
responses: {
|
|
3758
|
-
200: {
|
|
3759
|
-
description: "API is healthy",
|
|
3760
|
-
content: {
|
|
3761
|
-
"application/json": {
|
|
3762
|
-
schema: {
|
|
3763
|
-
type: "object",
|
|
3764
|
-
properties: {
|
|
3765
|
-
success: { type: "boolean", example: true },
|
|
3766
|
-
data: {
|
|
3767
|
-
type: "object",
|
|
3768
|
-
properties: {
|
|
3769
|
-
status: { type: "string", example: "ok" },
|
|
3770
|
-
uptime: { type: "number", description: "Process uptime in seconds" },
|
|
3771
|
-
timestamp: { type: "string", format: "date-time" },
|
|
3772
|
-
checks: {
|
|
3773
|
-
type: "object",
|
|
3774
|
-
properties: {
|
|
3775
|
-
liveness: { type: "string", example: "/health/live" },
|
|
3776
|
-
readiness: { type: "string", example: "/health/ready" }
|
|
3777
|
-
}
|
|
3778
|
-
}
|
|
3779
|
-
}
|
|
3780
|
-
}
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
}
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
}
|
|
3787
|
-
}
|
|
3788
|
-
};
|
|
3789
|
-
spec.paths["/health/live"] = {
|
|
3790
|
-
get: {
|
|
3791
|
-
tags: ["Health"],
|
|
3792
|
-
summary: "Liveness Probe",
|
|
3793
|
-
description: "Kubernetes liveness probe - checks if the application is alive. If this fails, Kubernetes will restart the pod.",
|
|
3794
|
-
responses: {
|
|
3795
|
-
200: {
|
|
3796
|
-
description: "Application is alive",
|
|
3797
|
-
content: {
|
|
3798
|
-
"application/json": {
|
|
3799
|
-
schema: {
|
|
3800
|
-
type: "object",
|
|
3801
|
-
properties: {
|
|
3802
|
-
success: { type: "boolean", example: true },
|
|
3803
|
-
data: {
|
|
3804
|
-
type: "object",
|
|
3805
|
-
properties: {
|
|
3806
|
-
status: { type: "string", example: "alive" },
|
|
3807
|
-
timestamp: { type: "string", format: "date-time" }
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
3812
|
-
}
|
|
3813
|
-
}
|
|
3814
|
-
}
|
|
3815
|
-
}
|
|
3816
|
-
}
|
|
3817
|
-
};
|
|
3818
|
-
spec.paths["/health/ready"] = {
|
|
3819
|
-
get: {
|
|
3820
|
-
tags: ["Health"],
|
|
3821
|
-
summary: "Readiness Probe",
|
|
3822
|
-
description: "Kubernetes readiness probe - checks if the application is ready to receive traffic. If this fails, Kubernetes will remove the pod from service endpoints.",
|
|
3823
|
-
responses: {
|
|
3824
|
-
200: {
|
|
3825
|
-
description: "Application is ready to receive traffic",
|
|
3826
|
-
content: {
|
|
3827
|
-
"application/json": {
|
|
3828
|
-
schema: {
|
|
3829
|
-
type: "object",
|
|
3830
|
-
properties: {
|
|
3831
|
-
success: { type: "boolean", example: true },
|
|
3832
|
-
data: {
|
|
3833
|
-
type: "object",
|
|
3834
|
-
properties: {
|
|
3835
|
-
status: { type: "string", example: "ready" },
|
|
3836
|
-
database: {
|
|
3837
|
-
type: "object",
|
|
3838
|
-
properties: {
|
|
3839
|
-
connected: { type: "boolean", example: true },
|
|
3840
|
-
resources: { type: "integer", example: 5 }
|
|
3841
|
-
}
|
|
3842
|
-
},
|
|
3843
|
-
timestamp: { type: "string", format: "date-time" }
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
}
|
|
3848
|
-
}
|
|
3849
|
-
}
|
|
3850
|
-
},
|
|
3851
|
-
503: {
|
|
3852
|
-
description: "Application is not ready",
|
|
3853
|
-
content: {
|
|
3854
|
-
"application/json": {
|
|
3855
|
-
schema: {
|
|
3856
|
-
type: "object",
|
|
3857
|
-
properties: {
|
|
3858
|
-
success: { type: "boolean", example: false },
|
|
3859
|
-
error: {
|
|
3860
|
-
type: "object",
|
|
3861
|
-
properties: {
|
|
3862
|
-
message: { type: "string", example: "Service not ready" },
|
|
3863
|
-
code: { type: "string", example: "NOT_READY" },
|
|
3864
|
-
details: {
|
|
3865
|
-
type: "object",
|
|
3866
|
-
properties: {
|
|
3867
|
-
database: {
|
|
3868
|
-
type: "object",
|
|
3869
|
-
properties: {
|
|
3870
|
-
connected: { type: "boolean", example: false },
|
|
3871
|
-
resources: { type: "integer", example: 0 }
|
|
3872
|
-
}
|
|
3873
|
-
}
|
|
3874
|
-
}
|
|
3875
|
-
}
|
|
3876
|
-
}
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
}
|
|
3880
|
-
}
|
|
3881
|
-
}
|
|
3882
|
-
}
|
|
3883
|
-
}
|
|
3884
|
-
}
|
|
3885
|
-
};
|
|
3886
|
-
spec.tags.push({
|
|
3887
|
-
name: "Health",
|
|
3888
|
-
description: "Health check endpoints for monitoring and Kubernetes probes"
|
|
3889
|
-
});
|
|
3890
|
-
const metricsPlugin = database.plugins?.metrics || database.plugins?.MetricsPlugin;
|
|
3891
|
-
if (metricsPlugin && metricsPlugin.config?.prometheus?.enabled) {
|
|
3892
|
-
const metricsPath = metricsPlugin.config.prometheus.path || "/metrics";
|
|
3893
|
-
const isIntegrated = metricsPlugin.config.prometheus.mode !== "standalone";
|
|
3894
|
-
if (isIntegrated) {
|
|
3895
|
-
spec.paths[metricsPath] = {
|
|
3896
|
-
get: {
|
|
3897
|
-
tags: ["Monitoring"],
|
|
3898
|
-
summary: "Prometheus Metrics",
|
|
3899
|
-
description: "Exposes application metrics in Prometheus text-based exposition format for monitoring and observability. Metrics include operation counts, durations, errors, uptime, and resource statistics.",
|
|
3900
|
-
responses: {
|
|
3901
|
-
200: {
|
|
3902
|
-
description: "Metrics in Prometheus format",
|
|
3903
|
-
content: {
|
|
3904
|
-
"text/plain": {
|
|
3905
|
-
schema: {
|
|
3906
|
-
type: "string",
|
|
3907
|
-
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'
|
|
3908
|
-
}
|
|
3909
|
-
}
|
|
3910
|
-
}
|
|
3911
|
-
}
|
|
3912
|
-
}
|
|
3913
|
-
}
|
|
3914
|
-
};
|
|
3915
|
-
spec.tags.push({
|
|
3916
|
-
name: "Monitoring",
|
|
3917
|
-
description: "Monitoring and observability endpoints (Prometheus)"
|
|
3918
|
-
});
|
|
3919
|
-
}
|
|
3920
|
-
}
|
|
3921
|
-
return spec;
|
|
3922
|
-
}
|
|
3923
|
-
|
|
3924
|
-
class ApiServer {
|
|
3925
|
-
/**
|
|
3926
|
-
* Create API server
|
|
3927
|
-
* @param {Object} options - Server options
|
|
3928
|
-
* @param {number} options.port - Server port
|
|
3929
|
-
* @param {string} options.host - Server host
|
|
3930
|
-
* @param {Object} options.database - s3db.js database instance
|
|
3931
|
-
* @param {Object} options.resources - Resource configuration
|
|
3932
|
-
* @param {Array} options.middlewares - Global middlewares
|
|
3933
|
-
*/
|
|
3934
|
-
constructor(options = {}) {
|
|
3935
|
-
this.options = {
|
|
3936
|
-
port: options.port || 3e3,
|
|
3937
|
-
host: options.host || "0.0.0.0",
|
|
3938
|
-
database: options.database,
|
|
3939
|
-
resources: options.resources || {},
|
|
3940
|
-
middlewares: options.middlewares || [],
|
|
3941
|
-
verbose: options.verbose || false,
|
|
3942
|
-
auth: options.auth || {},
|
|
3943
|
-
docsEnabled: options.docsEnabled !== false,
|
|
3944
|
-
// Enable /docs by default
|
|
3945
|
-
docsUI: options.docsUI || "redoc",
|
|
3946
|
-
// 'swagger' or 'redoc'
|
|
3947
|
-
maxBodySize: options.maxBodySize || 10 * 1024 * 1024,
|
|
3948
|
-
// 10MB default
|
|
3949
|
-
rootHandler: options.rootHandler,
|
|
3950
|
-
// Custom handler for root path, if not provided redirects to /docs
|
|
3951
|
-
apiInfo: {
|
|
3952
|
-
title: options.apiTitle || "s3db.js API",
|
|
3953
|
-
version: options.apiVersion || "1.0.0",
|
|
3954
|
-
description: options.apiDescription || "Auto-generated REST API for s3db.js resources"
|
|
3955
|
-
}
|
|
3956
|
-
};
|
|
3957
|
-
this.app = new hono.Hono();
|
|
3958
|
-
this.server = null;
|
|
3959
|
-
this.isRunning = false;
|
|
3960
|
-
this.openAPISpec = null;
|
|
3961
|
-
this._setupRoutes();
|
|
3962
|
-
}
|
|
3963
|
-
/**
|
|
3964
|
-
* Setup all routes
|
|
3965
|
-
* @private
|
|
3966
|
-
*/
|
|
3967
|
-
_setupRoutes() {
|
|
3968
|
-
this.options.middlewares.forEach((middleware) => {
|
|
3969
|
-
this.app.use("*", middleware);
|
|
3970
|
-
});
|
|
3971
|
-
this.app.use("*", async (c, next) => {
|
|
3972
|
-
const method = c.req.method;
|
|
3973
|
-
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
3974
|
-
const contentLength = c.req.header("content-length");
|
|
3975
|
-
if (contentLength) {
|
|
3976
|
-
const size = parseInt(contentLength);
|
|
3977
|
-
if (size > this.options.maxBodySize) {
|
|
3978
|
-
const response = payloadTooLarge(size, this.options.maxBodySize);
|
|
3979
|
-
c.header("Connection", "close");
|
|
3980
|
-
return c.json(response, response._status);
|
|
3981
|
-
}
|
|
3982
|
-
}
|
|
3983
|
-
}
|
|
3984
|
-
await next();
|
|
3985
|
-
});
|
|
3986
|
-
this.app.get("/health/live", (c) => {
|
|
3987
|
-
const response = success({
|
|
3988
|
-
status: "alive",
|
|
3989
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3990
|
-
});
|
|
3991
|
-
return c.json(response);
|
|
3992
|
-
});
|
|
3993
|
-
this.app.get("/health/ready", (c) => {
|
|
3994
|
-
const isReady = this.options.database && this.options.database.connected && Object.keys(this.options.database.resources).length > 0;
|
|
3995
|
-
if (!isReady) {
|
|
3996
|
-
const response2 = error("Service not ready", {
|
|
3997
|
-
status: 503,
|
|
3998
|
-
code: "NOT_READY",
|
|
3999
|
-
details: {
|
|
4000
|
-
database: {
|
|
4001
|
-
connected: this.options.database?.connected || false,
|
|
4002
|
-
resources: Object.keys(this.options.database?.resources || {}).length
|
|
4003
|
-
}
|
|
4004
|
-
}
|
|
4005
|
-
});
|
|
4006
|
-
return c.json(response2, 503);
|
|
4007
|
-
}
|
|
4008
|
-
const response = success({
|
|
4009
|
-
status: "ready",
|
|
4010
|
-
database: {
|
|
4011
|
-
connected: true,
|
|
4012
|
-
resources: Object.keys(this.options.database.resources).length
|
|
4013
|
-
},
|
|
4014
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4015
|
-
});
|
|
4016
|
-
return c.json(response);
|
|
4017
|
-
});
|
|
4018
|
-
this.app.get("/health", (c) => {
|
|
4019
|
-
const response = success({
|
|
4020
|
-
status: "ok",
|
|
4021
|
-
uptime: process.uptime(),
|
|
4022
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4023
|
-
checks: {
|
|
4024
|
-
liveness: "/health/live",
|
|
4025
|
-
readiness: "/health/ready"
|
|
4026
|
-
}
|
|
4027
|
-
});
|
|
4028
|
-
return c.json(response);
|
|
4029
|
-
});
|
|
4030
|
-
this.app.get("/", (c) => {
|
|
4031
|
-
if (this.options.rootHandler) {
|
|
4032
|
-
return this.options.rootHandler(c);
|
|
4033
|
-
}
|
|
4034
|
-
return c.redirect("/docs", 302);
|
|
4035
|
-
});
|
|
4036
|
-
if (this.options.docsEnabled) {
|
|
4037
|
-
this.app.get("/openapi.json", (c) => {
|
|
4038
|
-
if (!this.openAPISpec) {
|
|
4039
|
-
this.openAPISpec = this._generateOpenAPISpec();
|
|
4040
|
-
}
|
|
4041
|
-
return c.json(this.openAPISpec);
|
|
4042
|
-
});
|
|
4043
|
-
if (this.options.docsUI === "swagger") {
|
|
4044
|
-
this.app.get("/docs", swaggerUi.swaggerUI({
|
|
4045
|
-
url: "/openapi.json"
|
|
4046
|
-
}));
|
|
4047
|
-
} else {
|
|
4048
|
-
this.app.get("/docs", (c) => {
|
|
4049
|
-
return c.html(`<!DOCTYPE html>
|
|
4050
|
-
<html lang="en">
|
|
4051
|
-
<head>
|
|
4052
|
-
<meta charset="UTF-8">
|
|
4053
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4054
|
-
<title>${this.options.apiInfo.title} - API Documentation</title>
|
|
4055
|
-
<style>
|
|
4056
|
-
body {
|
|
4057
|
-
margin: 0;
|
|
4058
|
-
padding: 0;
|
|
4059
|
-
}
|
|
4060
|
-
</style>
|
|
4061
|
-
</head>
|
|
4062
|
-
<body>
|
|
4063
|
-
<redoc spec-url="/openapi.json"></redoc>
|
|
4064
|
-
<script src="https://cdn.redoc.ly/redoc/v2.5.1/bundles/redoc.standalone.js"><\/script>
|
|
4065
|
-
</body>
|
|
4066
|
-
</html>`);
|
|
4067
|
-
});
|
|
4068
|
-
}
|
|
4069
|
-
}
|
|
4070
|
-
this._setupResourceRoutes();
|
|
4071
|
-
this.app.onError((err, c) => {
|
|
4072
|
-
return errorHandler(err, c);
|
|
4073
|
-
});
|
|
4074
|
-
this.app.notFound((c) => {
|
|
4075
|
-
const response = error("Route not found", {
|
|
4076
|
-
status: 404,
|
|
4077
|
-
code: "NOT_FOUND",
|
|
4078
|
-
details: {
|
|
4079
|
-
path: c.req.path,
|
|
4080
|
-
method: c.req.method
|
|
4081
|
-
}
|
|
4082
|
-
});
|
|
4083
|
-
return c.json(response, 404);
|
|
4084
|
-
});
|
|
4085
|
-
}
|
|
4086
|
-
/**
|
|
4087
|
-
* Setup routes for all resources
|
|
4088
|
-
* @private
|
|
4089
|
-
*/
|
|
4090
|
-
_setupResourceRoutes() {
|
|
4091
|
-
const { database, resources: resourceConfigs } = this.options;
|
|
4092
|
-
const resources = database.resources;
|
|
4093
|
-
for (const [name, resource] of Object.entries(resources)) {
|
|
4094
|
-
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
4095
|
-
continue;
|
|
4096
|
-
}
|
|
4097
|
-
const config = resourceConfigs[name] || {
|
|
4098
|
-
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]};
|
|
4099
|
-
const version = resource.config?.currentVersion || resource.version || "v1";
|
|
4100
|
-
const resourceApp = createResourceRoutes(resource, version, {
|
|
4101
|
-
methods: config.methods,
|
|
4102
|
-
customMiddleware: config.customMiddleware || [],
|
|
4103
|
-
enableValidation: config.validation !== false
|
|
4104
|
-
});
|
|
4105
|
-
this.app.route(`/${version}/${name}`, resourceApp);
|
|
4106
|
-
if (this.options.verbose) {
|
|
4107
|
-
console.log(`[API Plugin] Mounted routes for resource '${name}' at /${version}/${name}`);
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
}
|
|
4111
|
-
/**
|
|
4112
|
-
* Start the server
|
|
4113
|
-
* @returns {Promise<void>}
|
|
4114
|
-
*/
|
|
4115
|
-
async start() {
|
|
4116
|
-
if (this.isRunning) {
|
|
4117
|
-
console.warn("[API Plugin] Server is already running");
|
|
4118
|
-
return;
|
|
4119
|
-
}
|
|
4120
|
-
const { port, host } = this.options;
|
|
4121
|
-
return new Promise((resolve, reject) => {
|
|
4122
|
-
try {
|
|
4123
|
-
this.server = nodeServer.serve({
|
|
4124
|
-
fetch: this.app.fetch,
|
|
4125
|
-
port,
|
|
4126
|
-
hostname: host
|
|
4127
|
-
}, (info) => {
|
|
4128
|
-
this.isRunning = true;
|
|
4129
|
-
console.log(`[API Plugin] Server listening on http://${info.address}:${info.port}`);
|
|
4130
|
-
resolve();
|
|
4131
|
-
});
|
|
4132
|
-
} catch (err) {
|
|
4133
|
-
reject(err);
|
|
4134
|
-
}
|
|
4135
|
-
});
|
|
4136
|
-
}
|
|
4137
|
-
/**
|
|
4138
|
-
* Stop the server
|
|
4139
|
-
* @returns {Promise<void>}
|
|
4140
|
-
*/
|
|
4141
|
-
async stop() {
|
|
4142
|
-
if (!this.isRunning) {
|
|
4143
|
-
console.warn("[API Plugin] Server is not running");
|
|
4144
|
-
return;
|
|
4145
|
-
}
|
|
4146
|
-
if (this.server && typeof this.server.close === "function") {
|
|
4147
|
-
await new Promise((resolve) => {
|
|
4148
|
-
this.server.close(() => {
|
|
4149
|
-
this.isRunning = false;
|
|
4150
|
-
console.log("[API Plugin] Server stopped");
|
|
4151
|
-
resolve();
|
|
4152
|
-
});
|
|
4153
|
-
});
|
|
4154
|
-
} else {
|
|
4155
|
-
this.isRunning = false;
|
|
4156
|
-
console.log("[API Plugin] Server stopped");
|
|
4157
|
-
}
|
|
4158
|
-
}
|
|
4159
|
-
/**
|
|
4160
|
-
* Get server info
|
|
4161
|
-
* @returns {Object} Server information
|
|
4162
|
-
*/
|
|
4163
|
-
getInfo() {
|
|
4164
|
-
return {
|
|
4165
|
-
isRunning: this.isRunning,
|
|
4166
|
-
port: this.options.port,
|
|
4167
|
-
host: this.options.host,
|
|
4168
|
-
resources: Object.keys(this.options.database.resources).length
|
|
4169
|
-
};
|
|
4170
|
-
}
|
|
4171
|
-
/**
|
|
4172
|
-
* Get Hono app instance
|
|
4173
|
-
* @returns {Hono} Hono app
|
|
4174
|
-
*/
|
|
4175
|
-
getApp() {
|
|
4176
|
-
return this.app;
|
|
4177
|
-
}
|
|
4178
|
-
/**
|
|
4179
|
-
* Generate OpenAPI specification
|
|
4180
|
-
* @private
|
|
4181
|
-
* @returns {Object} OpenAPI spec
|
|
4182
|
-
*/
|
|
4183
|
-
_generateOpenAPISpec() {
|
|
4184
|
-
const { port, host, database, resources, auth, apiInfo } = this.options;
|
|
4185
|
-
return generateOpenAPISpec(database, {
|
|
4186
|
-
title: apiInfo.title,
|
|
4187
|
-
version: apiInfo.version,
|
|
4188
|
-
description: apiInfo.description,
|
|
4189
|
-
serverUrl: `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`,
|
|
4190
|
-
auth,
|
|
4191
|
-
resources
|
|
4192
|
-
});
|
|
4193
|
-
}
|
|
4194
|
-
}
|
|
4195
|
-
|
|
4196
2474
|
const PLUGIN_DEPENDENCIES = {
|
|
4197
2475
|
"postgresql-replicator": {
|
|
4198
2476
|
name: "PostgreSQL Replicator",
|
|
2477
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
|
|
4199
2478
|
dependencies: {
|
|
4200
2479
|
"pg": {
|
|
4201
2480
|
version: "^8.0.0",
|
|
4202
2481
|
description: "PostgreSQL client for Node.js",
|
|
4203
|
-
installCommand: "pnpm add pg"
|
|
2482
|
+
installCommand: "pnpm add pg",
|
|
2483
|
+
npmUrl: "https://www.npmjs.com/package/pg"
|
|
4204
2484
|
}
|
|
4205
2485
|
}
|
|
4206
2486
|
},
|
|
4207
2487
|
"bigquery-replicator": {
|
|
4208
2488
|
name: "BigQuery Replicator",
|
|
2489
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
|
|
4209
2490
|
dependencies: {
|
|
4210
2491
|
"@google-cloud/bigquery": {
|
|
4211
2492
|
version: "^7.0.0",
|
|
4212
2493
|
description: "Google Cloud BigQuery SDK",
|
|
4213
|
-
installCommand: "pnpm add @google-cloud/bigquery"
|
|
2494
|
+
installCommand: "pnpm add @google-cloud/bigquery",
|
|
2495
|
+
npmUrl: "https://www.npmjs.com/package/@google-cloud/bigquery"
|
|
4214
2496
|
}
|
|
4215
2497
|
}
|
|
4216
2498
|
},
|
|
4217
2499
|
"sqs-replicator": {
|
|
4218
2500
|
name: "SQS Replicator",
|
|
2501
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
|
|
4219
2502
|
dependencies: {
|
|
4220
2503
|
"@aws-sdk/client-sqs": {
|
|
4221
2504
|
version: "^3.0.0",
|
|
4222
2505
|
description: "AWS SDK for SQS",
|
|
4223
|
-
installCommand: "pnpm add @aws-sdk/client-sqs"
|
|
2506
|
+
installCommand: "pnpm add @aws-sdk/client-sqs",
|
|
2507
|
+
npmUrl: "https://www.npmjs.com/package/@aws-sdk/client-sqs"
|
|
4224
2508
|
}
|
|
4225
2509
|
}
|
|
4226
2510
|
},
|
|
4227
2511
|
"sqs-consumer": {
|
|
4228
2512
|
name: "SQS Queue Consumer",
|
|
2513
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md",
|
|
4229
2514
|
dependencies: {
|
|
4230
2515
|
"@aws-sdk/client-sqs": {
|
|
4231
2516
|
version: "^3.0.0",
|
|
4232
2517
|
description: "AWS SDK for SQS",
|
|
4233
|
-
installCommand: "pnpm add @aws-sdk/client-sqs"
|
|
2518
|
+
installCommand: "pnpm add @aws-sdk/client-sqs",
|
|
2519
|
+
npmUrl: "https://www.npmjs.com/package/@aws-sdk/client-sqs"
|
|
4234
2520
|
}
|
|
4235
2521
|
}
|
|
4236
2522
|
},
|
|
4237
2523
|
"rabbitmq-consumer": {
|
|
4238
2524
|
name: "RabbitMQ Queue Consumer",
|
|
2525
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md",
|
|
4239
2526
|
dependencies: {
|
|
4240
2527
|
"amqplib": {
|
|
4241
2528
|
version: "^0.10.0",
|
|
4242
2529
|
description: "AMQP 0-9-1 library for RabbitMQ",
|
|
4243
|
-
installCommand: "pnpm add amqplib"
|
|
2530
|
+
installCommand: "pnpm add amqplib",
|
|
2531
|
+
npmUrl: "https://www.npmjs.com/package/amqplib"
|
|
4244
2532
|
}
|
|
4245
2533
|
}
|
|
4246
2534
|
},
|
|
4247
2535
|
"tfstate-plugin": {
|
|
4248
|
-
name: "
|
|
2536
|
+
name: "Tfstate Plugin",
|
|
2537
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/tfstate.md",
|
|
4249
2538
|
dependencies: {
|
|
4250
2539
|
"node-cron": {
|
|
4251
2540
|
version: "^4.0.0",
|
|
4252
2541
|
description: "Cron job scheduler for auto-sync functionality",
|
|
4253
|
-
installCommand: "pnpm add node-cron"
|
|
2542
|
+
installCommand: "pnpm add node-cron",
|
|
2543
|
+
npmUrl: "https://www.npmjs.com/package/node-cron"
|
|
4254
2544
|
}
|
|
4255
2545
|
}
|
|
4256
2546
|
},
|
|
4257
2547
|
"api-plugin": {
|
|
4258
2548
|
name: "API Plugin",
|
|
2549
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/api.md",
|
|
4259
2550
|
dependencies: {
|
|
4260
2551
|
"hono": {
|
|
4261
2552
|
version: "^4.0.0",
|
|
4262
2553
|
description: "Ultra-light HTTP server framework",
|
|
4263
|
-
installCommand: "pnpm add hono"
|
|
2554
|
+
installCommand: "pnpm add hono",
|
|
2555
|
+
npmUrl: "https://www.npmjs.com/package/hono"
|
|
4264
2556
|
},
|
|
4265
2557
|
"@hono/node-server": {
|
|
4266
2558
|
version: "^1.0.0",
|
|
4267
2559
|
description: "Node.js adapter for Hono",
|
|
4268
|
-
installCommand: "pnpm add @hono/node-server"
|
|
2560
|
+
installCommand: "pnpm add @hono/node-server",
|
|
2561
|
+
npmUrl: "https://www.npmjs.com/package/@hono/node-server"
|
|
4269
2562
|
},
|
|
4270
2563
|
"@hono/swagger-ui": {
|
|
4271
2564
|
version: "^0.4.0",
|
|
4272
2565
|
description: "Swagger UI integration for Hono",
|
|
4273
|
-
installCommand: "pnpm add @hono/swagger-ui"
|
|
2566
|
+
installCommand: "pnpm add @hono/swagger-ui",
|
|
2567
|
+
npmUrl: "https://www.npmjs.com/package/@hono/swagger-ui"
|
|
4274
2568
|
}
|
|
4275
2569
|
}
|
|
4276
2570
|
}
|
|
@@ -4356,21 +2650,55 @@ async function requirePluginDependency(pluginId, options = {}) {
|
|
|
4356
2650
|
}
|
|
4357
2651
|
const valid = missing.length === 0 && incompatible.length === 0;
|
|
4358
2652
|
if (!valid && throwOnError) {
|
|
2653
|
+
const depCount = Object.keys(pluginDef.dependencies).length;
|
|
2654
|
+
const missingCount = missing.length;
|
|
2655
|
+
const incompatCount = incompatible.length;
|
|
4359
2656
|
const errorMsg = [
|
|
4360
|
-
`
|
|
4361
|
-
${pluginDef.name} - Missing dependencies detected!
|
|
4362
|
-
`,
|
|
4363
|
-
`Plugin ID: ${pluginId}`,
|
|
4364
2657
|
"",
|
|
2658
|
+
"\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",
|
|
2659
|
+
`\u2551 \u274C ${pluginDef.name} - Missing Dependencies \u2551`,
|
|
2660
|
+
"\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",
|
|
2661
|
+
"",
|
|
2662
|
+
`\u{1F4E6} Plugin: ${pluginId}`,
|
|
2663
|
+
`\u{1F4CA} Status: ${depCount - missingCount - incompatCount}/${depCount} dependencies satisfied`,
|
|
2664
|
+
"",
|
|
2665
|
+
"\u{1F50D} Dependency Status:",
|
|
2666
|
+
"\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",
|
|
4365
2667
|
...messages,
|
|
4366
2668
|
"",
|
|
4367
|
-
"Quick
|
|
4368
|
-
|
|
2669
|
+
"\u{1F680} Quick Fix - Install Missing Dependencies:",
|
|
2670
|
+
"\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",
|
|
2671
|
+
"",
|
|
2672
|
+
" Option 1: Install individually",
|
|
2673
|
+
...Object.entries(pluginDef.dependencies).filter(([pkg]) => missing.includes(pkg) || incompatible.includes(pkg)).map(([pkg, info]) => ` ${info.installCommand}`),
|
|
2674
|
+
"",
|
|
2675
|
+
" Option 2: Install all at once",
|
|
2676
|
+
` pnpm add ${Object.keys(pluginDef.dependencies).join(" ")}`,
|
|
4369
2677
|
"",
|
|
4370
|
-
"
|
|
4371
|
-
`
|
|
2678
|
+
"\u{1F4DA} Documentation:",
|
|
2679
|
+
` ${pluginDef.docsUrl}`,
|
|
2680
|
+
"",
|
|
2681
|
+
"\u{1F4A1} Troubleshooting:",
|
|
2682
|
+
" \u2022 If packages are installed but not detected, try:",
|
|
2683
|
+
" 1. Delete node_modules and reinstall: rm -rf node_modules && pnpm install",
|
|
2684
|
+
" 2. Check Node.js version: node --version (requires Node 18+)",
|
|
2685
|
+
" 3. Verify pnpm version: pnpm --version (requires pnpm 8+)",
|
|
2686
|
+
"",
|
|
2687
|
+
" \u2022 Still having issues? Check:",
|
|
2688
|
+
" - Package.json has correct dependencies listed",
|
|
2689
|
+
" - No conflicting versions in pnpm-lock.yaml",
|
|
2690
|
+
" - File permissions (especially in node_modules/)",
|
|
2691
|
+
"",
|
|
2692
|
+
"\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",
|
|
2693
|
+
""
|
|
4372
2694
|
].join("\n");
|
|
4373
|
-
|
|
2695
|
+
const error = new Error(errorMsg);
|
|
2696
|
+
error.pluginId = pluginId;
|
|
2697
|
+
error.pluginName = pluginDef.name;
|
|
2698
|
+
error.missing = missing;
|
|
2699
|
+
error.incompatible = incompatible;
|
|
2700
|
+
error.docsUrl = pluginDef.docsUrl;
|
|
2701
|
+
throw error;
|
|
4374
2702
|
}
|
|
4375
2703
|
return { valid, missing, incompatible, messages };
|
|
4376
2704
|
}
|
|
@@ -4387,7 +2715,6 @@ class ApiPlugin extends Plugin {
|
|
|
4387
2715
|
port: options.port || 3e3,
|
|
4388
2716
|
host: options.host || "0.0.0.0",
|
|
4389
2717
|
verbose: options.verbose || false,
|
|
4390
|
-
// API Documentation (supports both new and legacy formats)
|
|
4391
2718
|
docs: {
|
|
4392
2719
|
enabled: options.docs?.enabled !== false && options.docsEnabled !== false,
|
|
4393
2720
|
// Enable by default
|
|
@@ -4704,6 +3031,11 @@ class ApiPlugin extends Plugin {
|
|
|
4704
3031
|
if (this.config.verbose) {
|
|
4705
3032
|
console.log("[API Plugin] Starting server...");
|
|
4706
3033
|
}
|
|
3034
|
+
const serverPath = "./server.js";
|
|
3035
|
+
const { ApiServer } = await import(
|
|
3036
|
+
/* @vite-ignore */
|
|
3037
|
+
serverPath
|
|
3038
|
+
);
|
|
4707
3039
|
this.server = new ApiServer({
|
|
4708
3040
|
port: this.config.port,
|
|
4709
3041
|
host: this.config.host,
|
|
@@ -5766,8 +4098,6 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
5766
4098
|
strategy: "all",
|
|
5767
4099
|
// 'all', 'any', 'priority'
|
|
5768
4100
|
concurrency: 3,
|
|
5769
|
-
requireAll: true,
|
|
5770
|
-
// For backward compatibility
|
|
5771
4101
|
...config
|
|
5772
4102
|
});
|
|
5773
4103
|
this.drivers = [];
|
|
@@ -6404,13 +4734,13 @@ class BackupPlugin extends Plugin {
|
|
|
6404
4734
|
createdAt: now.toISOString().slice(0, 10)
|
|
6405
4735
|
};
|
|
6406
4736
|
const [ok] = await tryFn(
|
|
6407
|
-
() => this.database.
|
|
4737
|
+
() => this.database.resources[this.config.backupMetadataResource].insert(metadata)
|
|
6408
4738
|
);
|
|
6409
4739
|
return metadata;
|
|
6410
4740
|
}
|
|
6411
4741
|
async _updateBackupMetadata(backupId, updates) {
|
|
6412
4742
|
const [ok] = await tryFn(
|
|
6413
|
-
() => this.database.
|
|
4743
|
+
() => this.database.resources[this.config.backupMetadataResource].update(backupId, updates)
|
|
6414
4744
|
);
|
|
6415
4745
|
}
|
|
6416
4746
|
async _createBackupManifest(type, options) {
|
|
@@ -6445,7 +4775,7 @@ class BackupPlugin extends Plugin {
|
|
|
6445
4775
|
let sinceTimestamp = null;
|
|
6446
4776
|
if (type === "incremental") {
|
|
6447
4777
|
const [lastBackupOk, , lastBackups] = await tryFn(
|
|
6448
|
-
() => this.database.
|
|
4778
|
+
() => this.database.resources[this.config.backupMetadataResource].list({
|
|
6449
4779
|
filter: {
|
|
6450
4780
|
status: "completed",
|
|
6451
4781
|
type: { $in: ["full", "incremental"] }
|
|
@@ -6753,7 +5083,7 @@ class BackupPlugin extends Plugin {
|
|
|
6753
5083
|
try {
|
|
6754
5084
|
const driverBackups = await this.driver.list(options);
|
|
6755
5085
|
const [metaOk, , metadataRecords] = await tryFn(
|
|
6756
|
-
() => this.database.
|
|
5086
|
+
() => this.database.resources[this.config.backupMetadataResource].list({
|
|
6757
5087
|
limit: options.limit || 50,
|
|
6758
5088
|
sort: { timestamp: -1 }
|
|
6759
5089
|
})
|
|
@@ -6781,14 +5111,14 @@ class BackupPlugin extends Plugin {
|
|
|
6781
5111
|
*/
|
|
6782
5112
|
async getBackupStatus(backupId) {
|
|
6783
5113
|
const [ok, , backup] = await tryFn(
|
|
6784
|
-
() => this.database.
|
|
5114
|
+
() => this.database.resources[this.config.backupMetadataResource].get(backupId)
|
|
6785
5115
|
);
|
|
6786
5116
|
return ok ? backup : null;
|
|
6787
5117
|
}
|
|
6788
5118
|
async _cleanupOldBackups() {
|
|
6789
5119
|
try {
|
|
6790
5120
|
const [listOk, , allBackups] = await tryFn(
|
|
6791
|
-
() => this.database.
|
|
5121
|
+
() => this.database.resources[this.config.backupMetadataResource].list({
|
|
6792
5122
|
filter: { status: "completed" },
|
|
6793
5123
|
sort: { timestamp: -1 }
|
|
6794
5124
|
})
|
|
@@ -6855,7 +5185,7 @@ class BackupPlugin extends Plugin {
|
|
|
6855
5185
|
for (const backup of backupsToDelete) {
|
|
6856
5186
|
try {
|
|
6857
5187
|
await this.driver.delete(backup.id, backup.driverInfo);
|
|
6858
|
-
await this.database.
|
|
5188
|
+
await this.database.resources[this.config.backupMetadataResource].delete(backup.id);
|
|
6859
5189
|
if (this.config.verbose) {
|
|
6860
5190
|
console.log(`[BackupPlugin] Deleted old backup: ${backup.id}`);
|
|
6861
5191
|
}
|
|
@@ -6891,12 +5221,6 @@ class BackupPlugin extends Plugin {
|
|
|
6891
5221
|
await this.driver.cleanup();
|
|
6892
5222
|
}
|
|
6893
5223
|
}
|
|
6894
|
-
/**
|
|
6895
|
-
* Cleanup plugin resources (alias for stop for backward compatibility)
|
|
6896
|
-
*/
|
|
6897
|
-
async cleanup() {
|
|
6898
|
-
await this.stop();
|
|
6899
|
-
}
|
|
6900
5224
|
}
|
|
6901
5225
|
|
|
6902
5226
|
class CacheError extends S3dbError {
|
|
@@ -9792,9 +8116,6 @@ async function consolidateRecord(originalId, transactionResource, targetResource
|
|
|
9792
8116
|
if (txnWithCohorts.cohortMonth && !txn.cohortMonth) {
|
|
9793
8117
|
updateData.cohortMonth = txnWithCohorts.cohortMonth;
|
|
9794
8118
|
}
|
|
9795
|
-
if (txn.value === null || txn.value === void 0) {
|
|
9796
|
-
updateData.value = 1;
|
|
9797
|
-
}
|
|
9798
8119
|
const [ok2, err2] = await tryFn(
|
|
9799
8120
|
() => transactionResource.update(txn.id, updateData)
|
|
9800
8121
|
);
|
|
@@ -11118,8 +9439,7 @@ async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
11118
9439
|
operation: "string|required",
|
|
11119
9440
|
timestamp: "string|required",
|
|
11120
9441
|
cohortDate: "string|required",
|
|
11121
|
-
cohortHour: "string|
|
|
11122
|
-
// ✅ FIX BUG #2: Changed from required to optional for migration compatibility
|
|
9442
|
+
cohortHour: "string|required",
|
|
11123
9443
|
cohortWeek: "string|optional",
|
|
11124
9444
|
cohortMonth: "string|optional",
|
|
11125
9445
|
source: "string|optional",
|
|
@@ -13453,7 +11773,7 @@ class RelationPlugin extends Plugin {
|
|
|
13453
11773
|
* @private
|
|
13454
11774
|
*/
|
|
13455
11775
|
async _setupResourceRelations(resourceName, relationsDef) {
|
|
13456
|
-
const resource = this.database.
|
|
11776
|
+
const resource = this.database.resources[resourceName];
|
|
13457
11777
|
if (!resource) {
|
|
13458
11778
|
if (this.verbose) {
|
|
13459
11779
|
console.warn(`[RelationPlugin] Resource "${resourceName}" not found, will setup when created`);
|
|
@@ -13564,7 +11884,7 @@ class RelationPlugin extends Plugin {
|
|
|
13564
11884
|
for (const record of records) {
|
|
13565
11885
|
const relatedData = record[relationName];
|
|
13566
11886
|
if (relatedData) {
|
|
13567
|
-
const relatedResource = this.database.
|
|
11887
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13568
11888
|
const relatedArray = Array.isArray(relatedData) ? relatedData : [relatedData];
|
|
13569
11889
|
if (relatedArray.length > 0) {
|
|
13570
11890
|
await this._eagerLoad(relatedArray, nestedIncludes, relatedResource);
|
|
@@ -13615,7 +11935,7 @@ class RelationPlugin extends Plugin {
|
|
|
13615
11935
|
* @private
|
|
13616
11936
|
*/
|
|
13617
11937
|
async _loadHasOne(records, relationName, config, sourceResource) {
|
|
13618
|
-
const relatedResource = this.database.
|
|
11938
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13619
11939
|
if (!relatedResource) {
|
|
13620
11940
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13621
11941
|
sourceResource: sourceResource.name,
|
|
@@ -13654,7 +11974,7 @@ class RelationPlugin extends Plugin {
|
|
|
13654
11974
|
* @private
|
|
13655
11975
|
*/
|
|
13656
11976
|
async _loadHasMany(records, relationName, config, sourceResource) {
|
|
13657
|
-
const relatedResource = this.database.
|
|
11977
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13658
11978
|
if (!relatedResource) {
|
|
13659
11979
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13660
11980
|
sourceResource: sourceResource.name,
|
|
@@ -13700,7 +12020,7 @@ class RelationPlugin extends Plugin {
|
|
|
13700
12020
|
* @private
|
|
13701
12021
|
*/
|
|
13702
12022
|
async _loadBelongsTo(records, relationName, config, sourceResource) {
|
|
13703
|
-
const relatedResource = this.database.
|
|
12023
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13704
12024
|
if (!relatedResource) {
|
|
13705
12025
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13706
12026
|
sourceResource: sourceResource.name,
|
|
@@ -13750,14 +12070,14 @@ class RelationPlugin extends Plugin {
|
|
|
13750
12070
|
* @private
|
|
13751
12071
|
*/
|
|
13752
12072
|
async _loadBelongsToMany(records, relationName, config, sourceResource) {
|
|
13753
|
-
const relatedResource = this.database.
|
|
12073
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13754
12074
|
if (!relatedResource) {
|
|
13755
12075
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13756
12076
|
sourceResource: sourceResource.name,
|
|
13757
12077
|
relation: relationName
|
|
13758
12078
|
});
|
|
13759
12079
|
}
|
|
13760
|
-
const junctionResource = this.database.
|
|
12080
|
+
const junctionResource = this.database.resources[config.through];
|
|
13761
12081
|
if (!junctionResource) {
|
|
13762
12082
|
throw new JunctionTableNotFoundError(config.through, {
|
|
13763
12083
|
sourceResource: sourceResource.name,
|
|
@@ -13941,7 +12261,7 @@ class RelationPlugin extends Plugin {
|
|
|
13941
12261
|
*/
|
|
13942
12262
|
async _cascadeDelete(record, resource, relationName, config) {
|
|
13943
12263
|
this.stats.cascadeOperations++;
|
|
13944
|
-
const relatedResource = this.database.
|
|
12264
|
+
const relatedResource = this.database.resources[config.resource];
|
|
13945
12265
|
if (!relatedResource) {
|
|
13946
12266
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
13947
12267
|
sourceResource: resource.name,
|
|
@@ -13949,7 +12269,7 @@ class RelationPlugin extends Plugin {
|
|
|
13949
12269
|
});
|
|
13950
12270
|
}
|
|
13951
12271
|
const deletedRecords = [];
|
|
13952
|
-
config.type === "belongsToMany" ? this.database.
|
|
12272
|
+
config.type === "belongsToMany" ? this.database.resources[config.through] : null;
|
|
13953
12273
|
try {
|
|
13954
12274
|
if (config.type === "hasMany") {
|
|
13955
12275
|
let relatedRecords;
|
|
@@ -14000,7 +12320,7 @@ class RelationPlugin extends Plugin {
|
|
|
14000
12320
|
await relatedResource.delete(relatedRecords[0].id);
|
|
14001
12321
|
}
|
|
14002
12322
|
} else if (config.type === "belongsToMany") {
|
|
14003
|
-
const junctionResource2 = this.database.
|
|
12323
|
+
const junctionResource2 = this.database.resources[config.through];
|
|
14004
12324
|
if (junctionResource2) {
|
|
14005
12325
|
let junctionRecords;
|
|
14006
12326
|
const partitionName = this._findPartitionByField(junctionResource2, config.foreignKey);
|
|
@@ -14068,7 +12388,7 @@ class RelationPlugin extends Plugin {
|
|
|
14068
12388
|
*/
|
|
14069
12389
|
async _cascadeUpdate(record, changes, resource, relationName, config) {
|
|
14070
12390
|
this.stats.cascadeOperations++;
|
|
14071
|
-
const relatedResource = this.database.
|
|
12391
|
+
const relatedResource = this.database.resources[config.resource];
|
|
14072
12392
|
if (!relatedResource) {
|
|
14073
12393
|
return;
|
|
14074
12394
|
}
|
|
@@ -19353,12 +17673,42 @@ ${errorDetails}`,
|
|
|
19353
17673
|
createdBy
|
|
19354
17674
|
};
|
|
19355
17675
|
this.hooks = {
|
|
17676
|
+
// Insert hooks
|
|
19356
17677
|
beforeInsert: [],
|
|
19357
17678
|
afterInsert: [],
|
|
17679
|
+
// Update hooks
|
|
19358
17680
|
beforeUpdate: [],
|
|
19359
17681
|
afterUpdate: [],
|
|
17682
|
+
// Delete hooks
|
|
19360
17683
|
beforeDelete: [],
|
|
19361
|
-
afterDelete: []
|
|
17684
|
+
afterDelete: [],
|
|
17685
|
+
// Get hooks
|
|
17686
|
+
beforeGet: [],
|
|
17687
|
+
afterGet: [],
|
|
17688
|
+
// List hooks
|
|
17689
|
+
beforeList: [],
|
|
17690
|
+
afterList: [],
|
|
17691
|
+
// Query hooks
|
|
17692
|
+
beforeQuery: [],
|
|
17693
|
+
afterQuery: [],
|
|
17694
|
+
// Patch hooks
|
|
17695
|
+
beforePatch: [],
|
|
17696
|
+
afterPatch: [],
|
|
17697
|
+
// Replace hooks
|
|
17698
|
+
beforeReplace: [],
|
|
17699
|
+
afterReplace: [],
|
|
17700
|
+
// Exists hooks
|
|
17701
|
+
beforeExists: [],
|
|
17702
|
+
afterExists: [],
|
|
17703
|
+
// Count hooks
|
|
17704
|
+
beforeCount: [],
|
|
17705
|
+
afterCount: [],
|
|
17706
|
+
// GetMany hooks
|
|
17707
|
+
beforeGetMany: [],
|
|
17708
|
+
afterGetMany: [],
|
|
17709
|
+
// DeleteMany hooks
|
|
17710
|
+
beforeDeleteMany: [],
|
|
17711
|
+
afterDeleteMany: []
|
|
19362
17712
|
};
|
|
19363
17713
|
this.attributes = attributes || {};
|
|
19364
17714
|
this.map = config.map;
|
|
@@ -19421,19 +17771,6 @@ ${errorDetails}`,
|
|
|
19421
17771
|
}
|
|
19422
17772
|
return idSize;
|
|
19423
17773
|
}
|
|
19424
|
-
/**
|
|
19425
|
-
* Get resource options (for backward compatibility with tests)
|
|
19426
|
-
*/
|
|
19427
|
-
get options() {
|
|
19428
|
-
return {
|
|
19429
|
-
timestamps: this.config.timestamps,
|
|
19430
|
-
partitions: this.config.partitions || {},
|
|
19431
|
-
cache: this.config.cache,
|
|
19432
|
-
autoDecrypt: this.config.autoDecrypt,
|
|
19433
|
-
paranoid: this.config.paranoid,
|
|
19434
|
-
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
19435
|
-
};
|
|
19436
|
-
}
|
|
19437
17774
|
export() {
|
|
19438
17775
|
const exported = this.schema.export();
|
|
19439
17776
|
exported.behavior = this.behavior;
|
|
@@ -19560,19 +17897,71 @@ ${errorDetails}`,
|
|
|
19560
17897
|
return data;
|
|
19561
17898
|
});
|
|
19562
17899
|
}
|
|
19563
|
-
|
|
17900
|
+
/**
|
|
17901
|
+
* Validate data against resource schema without saving
|
|
17902
|
+
* @param {Object} data - Data to validate
|
|
17903
|
+
* @param {Object} options - Validation options
|
|
17904
|
+
* @param {boolean} options.throwOnError - Throw error if validation fails (default: false)
|
|
17905
|
+
* @param {boolean} options.includeId - Include ID validation (default: false)
|
|
17906
|
+
* @param {boolean} options.mutateOriginal - Allow mutation of original data (default: false)
|
|
17907
|
+
* @returns {Promise<{valid: boolean, isValid: boolean, errors: Array, data: Object, original: Object}>} Validation result
|
|
17908
|
+
* @example
|
|
17909
|
+
* // Validate before insert
|
|
17910
|
+
* const result = await resource.validate({
|
|
17911
|
+
* name: 'John Doe',
|
|
17912
|
+
* email: 'invalid-email' // Will fail email validation
|
|
17913
|
+
* });
|
|
17914
|
+
*
|
|
17915
|
+
* if (!result.valid) {
|
|
17916
|
+
* console.log('Validation errors:', result.errors);
|
|
17917
|
+
* // [{ field: 'email', message: '...', ... }]
|
|
17918
|
+
* }
|
|
17919
|
+
*
|
|
17920
|
+
* // Throw on error
|
|
17921
|
+
* try {
|
|
17922
|
+
* await resource.validate({ email: 'bad' }, { throwOnError: true });
|
|
17923
|
+
* } catch (err) {
|
|
17924
|
+
* console.log('Validation failed:', err.message);
|
|
17925
|
+
* }
|
|
17926
|
+
*/
|
|
17927
|
+
async validate(data, options = {}) {
|
|
17928
|
+
const {
|
|
17929
|
+
throwOnError = false,
|
|
17930
|
+
includeId = false,
|
|
17931
|
+
mutateOriginal = false
|
|
17932
|
+
} = options;
|
|
17933
|
+
const dataToValidate = mutateOriginal ? data : lodashEs.cloneDeep(data);
|
|
17934
|
+
if (!includeId && dataToValidate.id) {
|
|
17935
|
+
delete dataToValidate.id;
|
|
17936
|
+
}
|
|
19564
17937
|
const result = {
|
|
19565
17938
|
original: lodashEs.cloneDeep(data),
|
|
19566
17939
|
isValid: false,
|
|
19567
|
-
errors: []
|
|
17940
|
+
errors: [],
|
|
17941
|
+
data: dataToValidate
|
|
19568
17942
|
};
|
|
19569
|
-
|
|
19570
|
-
|
|
19571
|
-
|
|
19572
|
-
|
|
19573
|
-
|
|
17943
|
+
try {
|
|
17944
|
+
const check = await this.schema.validate(dataToValidate, { mutateOriginal });
|
|
17945
|
+
if (check === true) {
|
|
17946
|
+
result.isValid = true;
|
|
17947
|
+
} else {
|
|
17948
|
+
result.errors = Array.isArray(check) ? check : [check];
|
|
17949
|
+
result.isValid = false;
|
|
17950
|
+
if (throwOnError) {
|
|
17951
|
+
const error = new Error("Validation failed");
|
|
17952
|
+
error.validationErrors = result.errors;
|
|
17953
|
+
error.invalidData = data;
|
|
17954
|
+
throw error;
|
|
17955
|
+
}
|
|
17956
|
+
}
|
|
17957
|
+
} catch (err) {
|
|
17958
|
+
if (!throwOnError) {
|
|
17959
|
+
result.errors = [{ message: err.message, error: err }];
|
|
17960
|
+
result.isValid = false;
|
|
17961
|
+
} else {
|
|
17962
|
+
throw err;
|
|
17963
|
+
}
|
|
19574
17964
|
}
|
|
19575
|
-
result.data = data;
|
|
19576
17965
|
return result;
|
|
19577
17966
|
}
|
|
19578
17967
|
/**
|
|
@@ -19837,12 +18226,12 @@ ${errorDetails}`,
|
|
|
19837
18226
|
const exists = await this.exists(id$1);
|
|
19838
18227
|
if (exists) throw new Error(`Resource with id '${id$1}' already exists`);
|
|
19839
18228
|
this.getResourceKey(id$1 || "(auto)");
|
|
19840
|
-
if (this.
|
|
18229
|
+
if (this.config.timestamps) {
|
|
19841
18230
|
attributes.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19842
18231
|
attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
19843
18232
|
}
|
|
19844
18233
|
const attributesWithDefaults = this.applyDefaults(attributes);
|
|
19845
|
-
const completeData = { id: id$1, ...attributesWithDefaults };
|
|
18234
|
+
const completeData = id$1 !== void 0 ? { id: id$1, ...attributesWithDefaults } : { ...attributesWithDefaults };
|
|
19846
18235
|
const preProcessedData = await this.executeHooks("beforeInsert", completeData);
|
|
19847
18236
|
const extraProps = Object.keys(preProcessedData).filter(
|
|
19848
18237
|
(k) => !(k in completeData) || preProcessedData[k] !== completeData[k]
|
|
@@ -19853,7 +18242,7 @@ ${errorDetails}`,
|
|
|
19853
18242
|
errors,
|
|
19854
18243
|
isValid,
|
|
19855
18244
|
data: validated
|
|
19856
|
-
} = await this.validate(preProcessedData);
|
|
18245
|
+
} = await this.validate(preProcessedData, { includeId: true });
|
|
19857
18246
|
if (!isValid) {
|
|
19858
18247
|
const errorMsg = errors && errors.length && errors[0].message ? errors[0].message : "Insert failed";
|
|
19859
18248
|
throw new InvalidResourceItem({
|
|
@@ -19957,6 +18346,7 @@ ${errorDetails}`,
|
|
|
19957
18346
|
async get(id) {
|
|
19958
18347
|
if (lodashEs.isObject(id)) throw new Error(`id cannot be an object`);
|
|
19959
18348
|
if (lodashEs.isEmpty(id)) throw new Error("id cannot be empty");
|
|
18349
|
+
await this.executeHooks("beforeGet", { id });
|
|
19960
18350
|
const key = this.getResourceKey(id);
|
|
19961
18351
|
const [ok, err, request] = await tryFn(() => this.client.getObject(key));
|
|
19962
18352
|
if (!ok) {
|
|
@@ -20005,17 +18395,67 @@ ${errorDetails}`,
|
|
|
20005
18395
|
if (objectVersion !== this.version) {
|
|
20006
18396
|
data = await this.applyVersionMapping(data, objectVersion, this.version);
|
|
20007
18397
|
}
|
|
18398
|
+
data = await this.executeHooks("afterGet", data);
|
|
20008
18399
|
this.emit("get", data);
|
|
20009
18400
|
const value = data;
|
|
20010
18401
|
return value;
|
|
20011
18402
|
}
|
|
18403
|
+
/**
|
|
18404
|
+
* Retrieve a resource object by ID, or return null if not found
|
|
18405
|
+
* @param {string} id - Resource ID
|
|
18406
|
+
* @returns {Promise<Object|null>} The resource object or null if not found
|
|
18407
|
+
* @example
|
|
18408
|
+
* const user = await resource.getOrNull('user-123');
|
|
18409
|
+
* if (user) {
|
|
18410
|
+
* console.log('Found user:', user.name);
|
|
18411
|
+
* } else {
|
|
18412
|
+
* console.log('User not found');
|
|
18413
|
+
* }
|
|
18414
|
+
*/
|
|
18415
|
+
async getOrNull(id) {
|
|
18416
|
+
const [ok, err, data] = await tryFn(() => this.get(id));
|
|
18417
|
+
if (!ok && err && (err.name === "NoSuchKey" || err.message?.includes("NoSuchKey"))) {
|
|
18418
|
+
return null;
|
|
18419
|
+
}
|
|
18420
|
+
if (!ok) {
|
|
18421
|
+
throw err;
|
|
18422
|
+
}
|
|
18423
|
+
return data;
|
|
18424
|
+
}
|
|
18425
|
+
/**
|
|
18426
|
+
* Retrieve a resource object by ID, or throw ResourceNotFoundError if not found
|
|
18427
|
+
* @param {string} id - Resource ID
|
|
18428
|
+
* @returns {Promise<Object>} The resource object
|
|
18429
|
+
* @throws {ResourceError} If resource does not exist
|
|
18430
|
+
* @example
|
|
18431
|
+
* // Throws error if user doesn't exist (no need for null check)
|
|
18432
|
+
* const user = await resource.getOrThrow('user-123');
|
|
18433
|
+
* console.log('User name:', user.name); // Safe to access
|
|
18434
|
+
*/
|
|
18435
|
+
async getOrThrow(id) {
|
|
18436
|
+
const [ok, err, data] = await tryFn(() => this.get(id));
|
|
18437
|
+
if (!ok && err && (err.name === "NoSuchKey" || err.message?.includes("NoSuchKey"))) {
|
|
18438
|
+
throw new ResourceError(`Resource '${this.name}' with id '${id}' not found`, {
|
|
18439
|
+
resourceName: this.name,
|
|
18440
|
+
operation: "getOrThrow",
|
|
18441
|
+
id,
|
|
18442
|
+
code: "RESOURCE_NOT_FOUND"
|
|
18443
|
+
});
|
|
18444
|
+
}
|
|
18445
|
+
if (!ok) {
|
|
18446
|
+
throw err;
|
|
18447
|
+
}
|
|
18448
|
+
return data;
|
|
18449
|
+
}
|
|
20012
18450
|
/**
|
|
20013
18451
|
* Check if a resource exists by ID
|
|
20014
18452
|
* @returns {Promise<boolean>} True if resource exists, false otherwise
|
|
20015
18453
|
*/
|
|
20016
18454
|
async exists(id) {
|
|
18455
|
+
await this.executeHooks("beforeExists", { id });
|
|
20017
18456
|
const key = this.getResourceKey(id);
|
|
20018
18457
|
const [ok, err] = await tryFn(() => this.client.headObject(key));
|
|
18458
|
+
await this.executeHooks("afterExists", { id, exists: ok });
|
|
20019
18459
|
return ok;
|
|
20020
18460
|
}
|
|
20021
18461
|
/**
|
|
@@ -20071,7 +18511,7 @@ ${errorDetails}`,
|
|
|
20071
18511
|
}
|
|
20072
18512
|
const preProcessedData = await this.executeHooks("beforeUpdate", lodashEs.cloneDeep(mergedData));
|
|
20073
18513
|
const completeData = { ...originalData, ...preProcessedData, id };
|
|
20074
|
-
const { isValid, errors, data } = await this.validate(lodashEs.cloneDeep(completeData));
|
|
18514
|
+
const { isValid, errors, data } = await this.validate(lodashEs.cloneDeep(completeData), { includeId: true });
|
|
20075
18515
|
if (!isValid) {
|
|
20076
18516
|
throw new InvalidResourceItem({
|
|
20077
18517
|
bucket: this.client.config.bucket,
|
|
@@ -20244,12 +18684,17 @@ ${errorDetails}`,
|
|
|
20244
18684
|
if (!fields || typeof fields !== "object") {
|
|
20245
18685
|
throw new Error("fields must be a non-empty object");
|
|
20246
18686
|
}
|
|
18687
|
+
await this.executeHooks("beforePatch", { id, fields, options });
|
|
20247
18688
|
const behavior = this.behavior;
|
|
20248
18689
|
const hasNestedFields = Object.keys(fields).some((key) => key.includes("."));
|
|
18690
|
+
let result;
|
|
20249
18691
|
if ((behavior === "enforce-limits" || behavior === "truncate-data") && !hasNestedFields) {
|
|
20250
|
-
|
|
18692
|
+
result = await this._patchViaCopyObject(id, fields, options);
|
|
18693
|
+
} else {
|
|
18694
|
+
result = await this.update(id, fields, options);
|
|
20251
18695
|
}
|
|
20252
|
-
|
|
18696
|
+
const finalResult = await this.executeHooks("afterPatch", result);
|
|
18697
|
+
return finalResult;
|
|
20253
18698
|
}
|
|
20254
18699
|
/**
|
|
20255
18700
|
* Internal helper: Optimized patch using HeadObject + CopyObject
|
|
@@ -20349,6 +18794,7 @@ ${errorDetails}`,
|
|
|
20349
18794
|
if (!fullData || typeof fullData !== "object") {
|
|
20350
18795
|
throw new Error("fullData must be a non-empty object");
|
|
20351
18796
|
}
|
|
18797
|
+
await this.executeHooks("beforeReplace", { id, fullData, options });
|
|
20352
18798
|
const { partition, partitionValues } = options;
|
|
20353
18799
|
const dataClone = lodashEs.cloneDeep(fullData);
|
|
20354
18800
|
const attributesWithDefaults = this.applyDefaults(dataClone);
|
|
@@ -20363,7 +18809,7 @@ ${errorDetails}`,
|
|
|
20363
18809
|
errors,
|
|
20364
18810
|
isValid,
|
|
20365
18811
|
data: validated
|
|
20366
|
-
} = await this.validate(completeData);
|
|
18812
|
+
} = await this.validate(completeData, { includeId: true });
|
|
20367
18813
|
if (!isValid) {
|
|
20368
18814
|
const errorMsg = errors && errors.length && errors[0].message ? errors[0].message : "Replace failed";
|
|
20369
18815
|
throw new InvalidResourceItem({
|
|
@@ -20436,7 +18882,8 @@ ${errorDetails}`,
|
|
|
20436
18882
|
await this.handlePartitionReferenceUpdates({}, replacedObject);
|
|
20437
18883
|
}
|
|
20438
18884
|
}
|
|
20439
|
-
|
|
18885
|
+
const finalResult = await this.executeHooks("afterReplace", replacedObject);
|
|
18886
|
+
return finalResult;
|
|
20440
18887
|
}
|
|
20441
18888
|
/**
|
|
20442
18889
|
* Update with conditional check (If-Match ETag)
|
|
@@ -20494,7 +18941,7 @@ ${errorDetails}`,
|
|
|
20494
18941
|
}
|
|
20495
18942
|
const preProcessedData = await this.executeHooks("beforeUpdate", lodashEs.cloneDeep(mergedData));
|
|
20496
18943
|
const completeData = { ...originalData, ...preProcessedData, id };
|
|
20497
|
-
const { isValid, errors, data } = await this.validate(lodashEs.cloneDeep(completeData));
|
|
18944
|
+
const { isValid, errors, data } = await this.validate(lodashEs.cloneDeep(completeData), { includeId: true });
|
|
20498
18945
|
if (!isValid) {
|
|
20499
18946
|
return {
|
|
20500
18947
|
success: false,
|
|
@@ -20715,6 +19162,7 @@ ${errorDetails}`,
|
|
|
20715
19162
|
* });
|
|
20716
19163
|
*/
|
|
20717
19164
|
async count({ partition = null, partitionValues = {} } = {}) {
|
|
19165
|
+
await this.executeHooks("beforeCount", { partition, partitionValues });
|
|
20718
19166
|
let prefix;
|
|
20719
19167
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
20720
19168
|
const partitionDef = this.config.partitions[partition];
|
|
@@ -20739,6 +19187,7 @@ ${errorDetails}`,
|
|
|
20739
19187
|
prefix = `resource=${this.name}/data`;
|
|
20740
19188
|
}
|
|
20741
19189
|
const count = await this.client.count({ prefix });
|
|
19190
|
+
await this.executeHooks("afterCount", { count, partition, partitionValues });
|
|
20742
19191
|
this.emit("count", count);
|
|
20743
19192
|
return count;
|
|
20744
19193
|
}
|
|
@@ -20774,6 +19223,7 @@ ${errorDetails}`,
|
|
|
20774
19223
|
* const results = await resource.deleteMany(deletedIds);
|
|
20775
19224
|
*/
|
|
20776
19225
|
async deleteMany(ids) {
|
|
19226
|
+
await this.executeHooks("beforeDeleteMany", { ids });
|
|
20777
19227
|
const packages = lodashEs.chunk(
|
|
20778
19228
|
ids.map((id) => this.getResourceKey(id)),
|
|
20779
19229
|
1e3
|
|
@@ -20795,6 +19245,7 @@ ${errorDetails}`,
|
|
|
20795
19245
|
});
|
|
20796
19246
|
return response;
|
|
20797
19247
|
});
|
|
19248
|
+
await this.executeHooks("afterDeleteMany", { ids, results });
|
|
20798
19249
|
this.emit("deleteMany", ids.length);
|
|
20799
19250
|
return results;
|
|
20800
19251
|
}
|
|
@@ -20916,6 +19367,7 @@ ${errorDetails}`,
|
|
|
20916
19367
|
* });
|
|
20917
19368
|
*/
|
|
20918
19369
|
async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
|
|
19370
|
+
await this.executeHooks("beforeList", { partition, partitionValues, limit, offset });
|
|
20919
19371
|
const [ok, err, result] = await tryFn(async () => {
|
|
20920
19372
|
if (!partition) {
|
|
20921
19373
|
return await this.listMain({ limit, offset });
|
|
@@ -20925,7 +19377,8 @@ ${errorDetails}`,
|
|
|
20925
19377
|
if (!ok) {
|
|
20926
19378
|
return this.handleListError(err, { partition, partitionValues });
|
|
20927
19379
|
}
|
|
20928
|
-
|
|
19380
|
+
const finalResult = await this.executeHooks("afterList", result);
|
|
19381
|
+
return finalResult;
|
|
20929
19382
|
}
|
|
20930
19383
|
async listMain({ limit, offset = 0 }) {
|
|
20931
19384
|
const [ok, err, ids] = await tryFn(() => this.listIds({ limit, offset }));
|
|
@@ -21068,6 +19521,7 @@ ${errorDetails}`,
|
|
|
21068
19521
|
* const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
|
|
21069
19522
|
*/
|
|
21070
19523
|
async getMany(ids) {
|
|
19524
|
+
await this.executeHooks("beforeGetMany", { ids });
|
|
21071
19525
|
const { results, errors } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
|
|
21072
19526
|
this.emit("error", error, content);
|
|
21073
19527
|
this.observers.map((x) => x.emit("error", this.name, error, content));
|
|
@@ -21088,8 +19542,9 @@ ${errorDetails}`,
|
|
|
21088
19542
|
}
|
|
21089
19543
|
throw err;
|
|
21090
19544
|
});
|
|
19545
|
+
const finalResults = await this.executeHooks("afterGetMany", results);
|
|
21091
19546
|
this.emit("getMany", ids.length);
|
|
21092
|
-
return
|
|
19547
|
+
return finalResults;
|
|
21093
19548
|
}
|
|
21094
19549
|
/**
|
|
21095
19550
|
* Get all resources (equivalent to list() without pagination)
|
|
@@ -21336,21 +19791,6 @@ ${errorDetails}`,
|
|
|
21336
19791
|
* @returns {Object} Schema object for the version
|
|
21337
19792
|
*/
|
|
21338
19793
|
async getSchemaForVersion(version) {
|
|
21339
|
-
if (version === this.version) {
|
|
21340
|
-
return this.schema;
|
|
21341
|
-
}
|
|
21342
|
-
const [ok, err, compatibleSchema] = await tryFn(() => Promise.resolve(new Schema({
|
|
21343
|
-
name: this.name,
|
|
21344
|
-
attributes: this.attributes,
|
|
21345
|
-
passphrase: this.passphrase,
|
|
21346
|
-
version,
|
|
21347
|
-
options: {
|
|
21348
|
-
...this.config,
|
|
21349
|
-
autoDecrypt: true,
|
|
21350
|
-
autoEncrypt: true
|
|
21351
|
-
}
|
|
21352
|
-
})));
|
|
21353
|
-
if (ok) return compatibleSchema;
|
|
21354
19794
|
return this.schema;
|
|
21355
19795
|
}
|
|
21356
19796
|
/**
|
|
@@ -21446,6 +19886,7 @@ ${errorDetails}`,
|
|
|
21446
19886
|
* );
|
|
21447
19887
|
*/
|
|
21448
19888
|
async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
|
|
19889
|
+
await this.executeHooks("beforeQuery", { filter, limit, offset, partition, partitionValues });
|
|
21449
19890
|
if (Object.keys(filter).length === 0) {
|
|
21450
19891
|
return await this.list({ partition, partitionValues, limit, offset });
|
|
21451
19892
|
}
|
|
@@ -21473,7 +19914,8 @@ ${errorDetails}`,
|
|
|
21473
19914
|
break;
|
|
21474
19915
|
}
|
|
21475
19916
|
}
|
|
21476
|
-
|
|
19917
|
+
const finalResults = results.slice(0, limit);
|
|
19918
|
+
return await this.executeHooks("afterQuery", finalResults);
|
|
21477
19919
|
}
|
|
21478
19920
|
/**
|
|
21479
19921
|
* Handle partition reference updates with change detection
|
|
@@ -21553,7 +19995,7 @@ ${errorDetails}`,
|
|
|
21553
19995
|
}
|
|
21554
19996
|
}
|
|
21555
19997
|
/**
|
|
21556
|
-
* Update partition objects to keep them in sync
|
|
19998
|
+
* Update partition objects to keep them in sync
|
|
21557
19999
|
* @param {Object} data - Updated object data
|
|
21558
20000
|
*/
|
|
21559
20001
|
async updatePartitionReferences(data) {
|
|
@@ -21939,7 +20381,32 @@ function validateResourceConfig(config) {
|
|
|
21939
20381
|
if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
|
|
21940
20382
|
errors.push("Resource 'hooks' must be an object");
|
|
21941
20383
|
} else {
|
|
21942
|
-
const validHookEvents = [
|
|
20384
|
+
const validHookEvents = [
|
|
20385
|
+
"beforeInsert",
|
|
20386
|
+
"afterInsert",
|
|
20387
|
+
"beforeUpdate",
|
|
20388
|
+
"afterUpdate",
|
|
20389
|
+
"beforeDelete",
|
|
20390
|
+
"afterDelete",
|
|
20391
|
+
"beforeGet",
|
|
20392
|
+
"afterGet",
|
|
20393
|
+
"beforeList",
|
|
20394
|
+
"afterList",
|
|
20395
|
+
"beforeQuery",
|
|
20396
|
+
"afterQuery",
|
|
20397
|
+
"beforeExists",
|
|
20398
|
+
"afterExists",
|
|
20399
|
+
"beforeCount",
|
|
20400
|
+
"afterCount",
|
|
20401
|
+
"beforePatch",
|
|
20402
|
+
"afterPatch",
|
|
20403
|
+
"beforeReplace",
|
|
20404
|
+
"afterReplace",
|
|
20405
|
+
"beforeGetMany",
|
|
20406
|
+
"afterGetMany",
|
|
20407
|
+
"beforeDeleteMany",
|
|
20408
|
+
"afterDeleteMany"
|
|
20409
|
+
];
|
|
21943
20410
|
for (const [event, hooksArr] of Object.entries(config.hooks)) {
|
|
21944
20411
|
if (!validHookEvents.includes(event)) {
|
|
21945
20412
|
errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(", ")}`);
|
|
@@ -21987,17 +20454,35 @@ class Database extends EventEmitter {
|
|
|
21987
20454
|
this.id = idGenerator(7);
|
|
21988
20455
|
this.version = "1";
|
|
21989
20456
|
this.s3dbVersion = (() => {
|
|
21990
|
-
const [ok, err, version] = tryFn(() => true ? "12.1
|
|
20457
|
+
const [ok, err, version] = tryFn(() => true ? "12.2.1" : "latest");
|
|
21991
20458
|
return ok ? version : "latest";
|
|
21992
20459
|
})();
|
|
21993
|
-
this.
|
|
20460
|
+
this._resourcesMap = {};
|
|
20461
|
+
this.resources = new Proxy(this._resourcesMap, {
|
|
20462
|
+
get: (target, prop) => {
|
|
20463
|
+
if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
|
|
20464
|
+
return target[prop];
|
|
20465
|
+
}
|
|
20466
|
+
if (target[prop]) {
|
|
20467
|
+
return target[prop];
|
|
20468
|
+
}
|
|
20469
|
+
return void 0;
|
|
20470
|
+
},
|
|
20471
|
+
// Support Object.keys(), Object.entries(), etc.
|
|
20472
|
+
ownKeys: (target) => {
|
|
20473
|
+
return Object.keys(target);
|
|
20474
|
+
},
|
|
20475
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
20476
|
+
return Object.getOwnPropertyDescriptor(target, prop);
|
|
20477
|
+
}
|
|
20478
|
+
});
|
|
21994
20479
|
this.savedMetadata = null;
|
|
21995
20480
|
this.options = options;
|
|
21996
20481
|
this.verbose = options.verbose || false;
|
|
21997
20482
|
this.parallelism = parseInt(options.parallelism + "") || 10;
|
|
21998
|
-
this.plugins = options.plugins || [];
|
|
21999
|
-
this.pluginRegistry = {};
|
|
22000
20483
|
this.pluginList = options.plugins || [];
|
|
20484
|
+
this.pluginRegistry = {};
|
|
20485
|
+
this.plugins = this.pluginRegistry;
|
|
22001
20486
|
this.cache = options.cache;
|
|
22002
20487
|
this.passphrase = options.passphrase || "secret";
|
|
22003
20488
|
this.versioningEnabled = options.versioningEnabled || false;
|
|
@@ -22103,7 +20588,7 @@ class Database extends EventEmitter {
|
|
|
22103
20588
|
} else {
|
|
22104
20589
|
restoredIdSize = versionData.idSize || 22;
|
|
22105
20590
|
}
|
|
22106
|
-
this.
|
|
20591
|
+
this._resourcesMap[name] = new Resource({
|
|
22107
20592
|
name,
|
|
22108
20593
|
client: this.client,
|
|
22109
20594
|
database: this,
|
|
@@ -22172,7 +20657,7 @@ class Database extends EventEmitter {
|
|
|
22172
20657
|
}
|
|
22173
20658
|
}
|
|
22174
20659
|
for (const [name, savedResource] of Object.entries(savedMetadata.resources || {})) {
|
|
22175
|
-
if (!this.
|
|
20660
|
+
if (!this._resourcesMap[name]) {
|
|
22176
20661
|
const currentVersion = savedResource.currentVersion || "v1";
|
|
22177
20662
|
const versionData = savedResource.versions?.[currentVersion];
|
|
22178
20663
|
changes.push({
|
|
@@ -22684,7 +21169,7 @@ class Database extends EventEmitter {
|
|
|
22684
21169
|
* @returns {boolean} True if resource exists, false otherwise
|
|
22685
21170
|
*/
|
|
22686
21171
|
resourceExists(name) {
|
|
22687
|
-
return !!this.
|
|
21172
|
+
return !!this._resourcesMap[name];
|
|
22688
21173
|
}
|
|
22689
21174
|
/**
|
|
22690
21175
|
* Check if a resource exists with the same definition hash
|
|
@@ -22692,14 +21177,13 @@ class Database extends EventEmitter {
|
|
|
22692
21177
|
* @param {string} config.name - Resource name
|
|
22693
21178
|
* @param {Object} config.attributes - Resource attributes
|
|
22694
21179
|
* @param {string} [config.behavior] - Resource behavior
|
|
22695
|
-
* @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
|
|
22696
21180
|
* @returns {Object} Result with exists and hash information
|
|
22697
21181
|
*/
|
|
22698
|
-
resourceExistsWithSameHash({ name, attributes, behavior = "user-managed", partitions = {}
|
|
22699
|
-
if (!this.
|
|
21182
|
+
resourceExistsWithSameHash({ name, attributes, behavior = "user-managed", partitions = {} }) {
|
|
21183
|
+
if (!this._resourcesMap[name]) {
|
|
22700
21184
|
return { exists: false, sameHash: false, hash: null };
|
|
22701
21185
|
}
|
|
22702
|
-
const existingResource = this.
|
|
21186
|
+
const existingResource = this._resourcesMap[name];
|
|
22703
21187
|
const existingHash = this.generateDefinitionHash(existingResource.export());
|
|
22704
21188
|
const mockResource = new Resource({
|
|
22705
21189
|
name,
|
|
@@ -22709,8 +21193,7 @@ class Database extends EventEmitter {
|
|
|
22709
21193
|
client: this.client,
|
|
22710
21194
|
version: existingResource.version,
|
|
22711
21195
|
passphrase: this.passphrase,
|
|
22712
|
-
versioningEnabled: this.versioningEnabled
|
|
22713
|
-
...options
|
|
21196
|
+
versioningEnabled: this.versioningEnabled
|
|
22714
21197
|
});
|
|
22715
21198
|
const newHash = this.generateDefinitionHash(mockResource.export());
|
|
22716
21199
|
return {
|
|
@@ -22738,12 +21221,49 @@ class Database extends EventEmitter {
|
|
|
22738
21221
|
* @param {string} [config.createdBy='user'] - Who created this resource ('user', 'plugin', or plugin name)
|
|
22739
21222
|
* @returns {Promise<Resource>} The created or updated resource
|
|
22740
21223
|
*/
|
|
22741
|
-
|
|
22742
|
-
|
|
22743
|
-
|
|
21224
|
+
/**
|
|
21225
|
+
* Normalize partitions config from array or object format
|
|
21226
|
+
* @param {Array|Object} partitions - Partitions config
|
|
21227
|
+
* @param {Object} attributes - Resource attributes
|
|
21228
|
+
* @returns {Object} Normalized partitions object
|
|
21229
|
+
* @private
|
|
21230
|
+
*/
|
|
21231
|
+
_normalizePartitions(partitions, attributes) {
|
|
21232
|
+
if (!Array.isArray(partitions)) {
|
|
21233
|
+
return partitions || {};
|
|
21234
|
+
}
|
|
21235
|
+
const normalized = {};
|
|
21236
|
+
for (const fieldName of partitions) {
|
|
21237
|
+
if (typeof fieldName !== "string") {
|
|
21238
|
+
throw new Error(`Partition field must be a string, got ${typeof fieldName}`);
|
|
21239
|
+
}
|
|
21240
|
+
if (!attributes[fieldName]) {
|
|
21241
|
+
throw new Error(`Partition field '${fieldName}' not found in attributes`);
|
|
21242
|
+
}
|
|
21243
|
+
const partitionName = `by${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
|
|
21244
|
+
const fieldDef = attributes[fieldName];
|
|
21245
|
+
let fieldType = "string";
|
|
21246
|
+
if (typeof fieldDef === "string") {
|
|
21247
|
+
fieldType = fieldDef.split("|")[0].trim();
|
|
21248
|
+
} else if (typeof fieldDef === "object" && fieldDef.type) {
|
|
21249
|
+
fieldType = fieldDef.type;
|
|
21250
|
+
}
|
|
21251
|
+
normalized[partitionName] = {
|
|
21252
|
+
fields: {
|
|
21253
|
+
[fieldName]: fieldType
|
|
21254
|
+
}
|
|
21255
|
+
};
|
|
21256
|
+
}
|
|
21257
|
+
return normalized;
|
|
21258
|
+
}
|
|
21259
|
+
async createResource({ name, attributes, behavior = "user-managed", hooks, middlewares, ...config }) {
|
|
21260
|
+
const normalizedPartitions = this._normalizePartitions(config.partitions, attributes);
|
|
21261
|
+
if (this._resourcesMap[name]) {
|
|
21262
|
+
const existingResource = this._resourcesMap[name];
|
|
22744
21263
|
Object.assign(existingResource.config, {
|
|
22745
21264
|
cache: this.cache,
|
|
22746
|
-
...config
|
|
21265
|
+
...config,
|
|
21266
|
+
partitions: normalizedPartitions
|
|
22747
21267
|
});
|
|
22748
21268
|
if (behavior) {
|
|
22749
21269
|
existingResource.behavior = behavior;
|
|
@@ -22761,6 +21281,9 @@ class Database extends EventEmitter {
|
|
|
22761
21281
|
}
|
|
22762
21282
|
}
|
|
22763
21283
|
}
|
|
21284
|
+
if (middlewares) {
|
|
21285
|
+
this._applyMiddlewares(existingResource, middlewares);
|
|
21286
|
+
}
|
|
22764
21287
|
const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
|
|
22765
21288
|
const existingMetadata2 = this.savedMetadata?.resources?.[name];
|
|
22766
21289
|
const currentVersion = existingMetadata2?.currentVersion || "v1";
|
|
@@ -22784,7 +21307,7 @@ class Database extends EventEmitter {
|
|
|
22784
21307
|
observers: [this],
|
|
22785
21308
|
cache: config.cache !== void 0 ? config.cache : this.cache,
|
|
22786
21309
|
timestamps: config.timestamps !== void 0 ? config.timestamps : false,
|
|
22787
|
-
partitions:
|
|
21310
|
+
partitions: normalizedPartitions,
|
|
22788
21311
|
paranoid: config.paranoid !== void 0 ? config.paranoid : true,
|
|
22789
21312
|
allNestedObjectsOptional: config.allNestedObjectsOptional !== void 0 ? config.allNestedObjectsOptional : true,
|
|
22790
21313
|
autoDecrypt: config.autoDecrypt !== void 0 ? config.autoDecrypt : true,
|
|
@@ -22800,16 +21323,96 @@ class Database extends EventEmitter {
|
|
|
22800
21323
|
createdBy: config.createdBy || "user"
|
|
22801
21324
|
});
|
|
22802
21325
|
resource.database = this;
|
|
22803
|
-
this.
|
|
21326
|
+
this._resourcesMap[name] = resource;
|
|
21327
|
+
if (middlewares) {
|
|
21328
|
+
this._applyMiddlewares(resource, middlewares);
|
|
21329
|
+
}
|
|
22804
21330
|
await this.uploadMetadataFile();
|
|
22805
21331
|
this.emit("s3db.resourceCreated", name);
|
|
22806
21332
|
return resource;
|
|
22807
21333
|
}
|
|
22808
|
-
|
|
22809
|
-
|
|
22810
|
-
|
|
21334
|
+
/**
|
|
21335
|
+
* Apply middlewares to a resource
|
|
21336
|
+
* @param {Resource} resource - Resource instance
|
|
21337
|
+
* @param {Array|Object} middlewares - Middlewares config
|
|
21338
|
+
* @private
|
|
21339
|
+
*/
|
|
21340
|
+
_applyMiddlewares(resource, middlewares) {
|
|
21341
|
+
if (Array.isArray(middlewares)) {
|
|
21342
|
+
const methods = resource._middlewareMethods || [
|
|
21343
|
+
"get",
|
|
21344
|
+
"list",
|
|
21345
|
+
"listIds",
|
|
21346
|
+
"getAll",
|
|
21347
|
+
"count",
|
|
21348
|
+
"page",
|
|
21349
|
+
"insert",
|
|
21350
|
+
"update",
|
|
21351
|
+
"delete",
|
|
21352
|
+
"deleteMany",
|
|
21353
|
+
"exists",
|
|
21354
|
+
"getMany",
|
|
21355
|
+
"content",
|
|
21356
|
+
"hasContent",
|
|
21357
|
+
"query",
|
|
21358
|
+
"getFromPartition",
|
|
21359
|
+
"setContent",
|
|
21360
|
+
"deleteContent",
|
|
21361
|
+
"replace",
|
|
21362
|
+
"patch"
|
|
21363
|
+
];
|
|
21364
|
+
for (const method of methods) {
|
|
21365
|
+
for (const middleware of middlewares) {
|
|
21366
|
+
if (typeof middleware === "function") {
|
|
21367
|
+
resource.useMiddleware(method, middleware);
|
|
21368
|
+
}
|
|
21369
|
+
}
|
|
21370
|
+
}
|
|
21371
|
+
return;
|
|
21372
|
+
}
|
|
21373
|
+
if (typeof middlewares === "object" && middlewares !== null) {
|
|
21374
|
+
for (const [method, fns] of Object.entries(middlewares)) {
|
|
21375
|
+
if (method === "*") {
|
|
21376
|
+
const methods = resource._middlewareMethods || [
|
|
21377
|
+
"get",
|
|
21378
|
+
"list",
|
|
21379
|
+
"listIds",
|
|
21380
|
+
"getAll",
|
|
21381
|
+
"count",
|
|
21382
|
+
"page",
|
|
21383
|
+
"insert",
|
|
21384
|
+
"update",
|
|
21385
|
+
"delete",
|
|
21386
|
+
"deleteMany",
|
|
21387
|
+
"exists",
|
|
21388
|
+
"getMany",
|
|
21389
|
+
"content",
|
|
21390
|
+
"hasContent",
|
|
21391
|
+
"query",
|
|
21392
|
+
"getFromPartition",
|
|
21393
|
+
"setContent",
|
|
21394
|
+
"deleteContent",
|
|
21395
|
+
"replace",
|
|
21396
|
+
"patch"
|
|
21397
|
+
];
|
|
21398
|
+
const middlewareArray = Array.isArray(fns) ? fns : [fns];
|
|
21399
|
+
for (const targetMethod of methods) {
|
|
21400
|
+
for (const middleware of middlewareArray) {
|
|
21401
|
+
if (typeof middleware === "function") {
|
|
21402
|
+
resource.useMiddleware(targetMethod, middleware);
|
|
21403
|
+
}
|
|
21404
|
+
}
|
|
21405
|
+
}
|
|
21406
|
+
} else {
|
|
21407
|
+
const middlewareArray = Array.isArray(fns) ? fns : [fns];
|
|
21408
|
+
for (const middleware of middlewareArray) {
|
|
21409
|
+
if (typeof middleware === "function") {
|
|
21410
|
+
resource.useMiddleware(method, middleware);
|
|
21411
|
+
}
|
|
21412
|
+
}
|
|
21413
|
+
}
|
|
21414
|
+
}
|
|
22811
21415
|
}
|
|
22812
|
-
return this.resources[name];
|
|
22813
21416
|
}
|
|
22814
21417
|
/**
|
|
22815
21418
|
* List all resource names
|
|
@@ -22824,14 +21427,14 @@ class Database extends EventEmitter {
|
|
|
22824
21427
|
* @returns {Resource} Resource instance
|
|
22825
21428
|
*/
|
|
22826
21429
|
async getResource(name) {
|
|
22827
|
-
if (!this.
|
|
21430
|
+
if (!this._resourcesMap[name]) {
|
|
22828
21431
|
throw new ResourceNotFound({
|
|
22829
21432
|
bucket: this.client.config.bucket,
|
|
22830
21433
|
resourceName: name,
|
|
22831
21434
|
id: name
|
|
22832
21435
|
});
|
|
22833
21436
|
}
|
|
22834
|
-
return this.
|
|
21437
|
+
return this._resourcesMap[name];
|
|
22835
21438
|
}
|
|
22836
21439
|
/**
|
|
22837
21440
|
* Get database configuration
|
|
@@ -22884,7 +21487,7 @@ class Database extends EventEmitter {
|
|
|
22884
21487
|
}
|
|
22885
21488
|
});
|
|
22886
21489
|
}
|
|
22887
|
-
Object.keys(this.resources).forEach((k) => delete this.
|
|
21490
|
+
Object.keys(this.resources).forEach((k) => delete this._resourcesMap[k]);
|
|
22888
21491
|
}
|
|
22889
21492
|
if (this.client && typeof this.client.removeAllListeners === "function") {
|
|
22890
21493
|
this.client.removeAllListeners();
|
|
@@ -24628,14 +23231,6 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24628
23231
|
}
|
|
24629
23232
|
async start() {
|
|
24630
23233
|
}
|
|
24631
|
-
async stop() {
|
|
24632
|
-
for (const replicator of this.replicators || []) {
|
|
24633
|
-
if (replicator && typeof replicator.cleanup === "function") {
|
|
24634
|
-
await replicator.cleanup();
|
|
24635
|
-
}
|
|
24636
|
-
}
|
|
24637
|
-
this.removeDatabaseHooks();
|
|
24638
|
-
}
|
|
24639
23234
|
installDatabaseHooks() {
|
|
24640
23235
|
this._afterCreateResourceHook = (resource) => {
|
|
24641
23236
|
if (resource.name !== (this.config.replicatorLogResource || "plg_replicator_logs")) {
|
|
@@ -24965,20 +23560,20 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24965
23560
|
}
|
|
24966
23561
|
this.emit("replicator.sync.completed", { replicatorId, stats: this.stats });
|
|
24967
23562
|
}
|
|
24968
|
-
async
|
|
23563
|
+
async stop() {
|
|
24969
23564
|
const [ok, error] = await tryFn(async () => {
|
|
24970
23565
|
if (this.replicators && this.replicators.length > 0) {
|
|
24971
23566
|
const cleanupPromises = this.replicators.map(async (replicator) => {
|
|
24972
23567
|
const [replicatorOk, replicatorError] = await tryFn(async () => {
|
|
24973
|
-
if (replicator && typeof replicator.
|
|
24974
|
-
await replicator.
|
|
23568
|
+
if (replicator && typeof replicator.stop === "function") {
|
|
23569
|
+
await replicator.stop();
|
|
24975
23570
|
}
|
|
24976
23571
|
});
|
|
24977
23572
|
if (!replicatorOk) {
|
|
24978
23573
|
if (this.config.verbose) {
|
|
24979
|
-
console.warn(`[ReplicatorPlugin] Failed to
|
|
23574
|
+
console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
|
|
24980
23575
|
}
|
|
24981
|
-
this.emit("
|
|
23576
|
+
this.emit("replicator_stop_error", {
|
|
24982
23577
|
replicator: replicator.name || replicator.id || "unknown",
|
|
24983
23578
|
driver: replicator.driver || "unknown",
|
|
24984
23579
|
error: replicatorError.message
|
|
@@ -24987,6 +23582,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
24987
23582
|
});
|
|
24988
23583
|
await Promise.allSettled(cleanupPromises);
|
|
24989
23584
|
}
|
|
23585
|
+
this.removeDatabaseHooks();
|
|
24990
23586
|
if (this.database && this.database.resources) {
|
|
24991
23587
|
for (const resourceName of this.eventListenersInstalled) {
|
|
24992
23588
|
const resource = this.database.resources[resourceName];
|
|
@@ -25006,9 +23602,9 @@ class ReplicatorPlugin extends Plugin {
|
|
|
25006
23602
|
});
|
|
25007
23603
|
if (!ok) {
|
|
25008
23604
|
if (this.config.verbose) {
|
|
25009
|
-
console.warn(`[ReplicatorPlugin] Failed to
|
|
23605
|
+
console.warn(`[ReplicatorPlugin] Failed to stop plugin: ${error.message}`);
|
|
25010
23606
|
}
|
|
25011
|
-
this.emit("
|
|
23607
|
+
this.emit("replicator_plugin_stop_error", {
|
|
25012
23608
|
error: error.message
|
|
25013
23609
|
});
|
|
25014
23610
|
}
|
|
@@ -25828,7 +24424,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
25828
24424
|
}
|
|
25829
24425
|
async _persistJobExecution(jobName, executionId, startTime, endTime, duration, status, result, error, retryCount) {
|
|
25830
24426
|
const [ok, err] = await tryFn(
|
|
25831
|
-
() => this.database.
|
|
24427
|
+
() => this.database.resources[this.config.jobHistoryResource].insert({
|
|
25832
24428
|
id: executionId,
|
|
25833
24429
|
jobName,
|
|
25834
24430
|
status,
|
|
@@ -25969,7 +24565,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
25969
24565
|
queryParams.status = status;
|
|
25970
24566
|
}
|
|
25971
24567
|
const [ok, err, history] = await tryFn(
|
|
25972
|
-
() => this.database.
|
|
24568
|
+
() => this.database.resources[this.config.jobHistoryResource].query(queryParams)
|
|
25973
24569
|
);
|
|
25974
24570
|
if (!ok) {
|
|
25975
24571
|
if (this.config.verbose) {
|
|
@@ -26108,9 +24704,6 @@ class SchedulerPlugin extends Plugin {
|
|
|
26108
24704
|
if (this._isTestEnvironment()) {
|
|
26109
24705
|
this.activeJobs.clear();
|
|
26110
24706
|
}
|
|
26111
|
-
}
|
|
26112
|
-
async cleanup() {
|
|
26113
|
-
await this.stop();
|
|
26114
24707
|
this.jobs.clear();
|
|
26115
24708
|
this.statistics.clear();
|
|
26116
24709
|
this.activeJobs.clear();
|
|
@@ -26359,7 +24952,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26359
24952
|
let lastLogErr;
|
|
26360
24953
|
for (let attempt = 0; attempt < this.config.retryAttempts; attempt++) {
|
|
26361
24954
|
const [ok, err] = await tryFn(
|
|
26362
|
-
() => this.database.
|
|
24955
|
+
() => this.database.resources[this.config.transitionLogResource].insert({
|
|
26363
24956
|
id: transitionId,
|
|
26364
24957
|
machineId,
|
|
26365
24958
|
entityId,
|
|
@@ -26395,11 +24988,11 @@ class StateMachinePlugin extends Plugin {
|
|
|
26395
24988
|
updatedAt: now
|
|
26396
24989
|
};
|
|
26397
24990
|
const [updateOk] = await tryFn(
|
|
26398
|
-
() => this.database.
|
|
24991
|
+
() => this.database.resources[this.config.stateResource].update(stateId, stateData)
|
|
26399
24992
|
);
|
|
26400
24993
|
if (!updateOk) {
|
|
26401
24994
|
const [insertOk, insertErr] = await tryFn(
|
|
26402
|
-
() => this.database.
|
|
24995
|
+
() => this.database.resources[this.config.stateResource].insert({ id: stateId, ...stateData })
|
|
26403
24996
|
);
|
|
26404
24997
|
if (!insertOk && this.config.verbose) {
|
|
26405
24998
|
console.warn(`[StateMachinePlugin] Failed to upsert state:`, insertErr.message);
|
|
@@ -26462,7 +25055,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26462
25055
|
if (this.config.persistTransitions) {
|
|
26463
25056
|
const stateId = `${machineId}_${entityId}`;
|
|
26464
25057
|
const [ok, err, stateRecord] = await tryFn(
|
|
26465
|
-
() => this.database.
|
|
25058
|
+
() => this.database.resources[this.config.stateResource].get(stateId)
|
|
26466
25059
|
);
|
|
26467
25060
|
if (ok && stateRecord) {
|
|
26468
25061
|
machine.currentStates.set(entityId, stateRecord.currentState);
|
|
@@ -26505,7 +25098,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26505
25098
|
}
|
|
26506
25099
|
const { limit = 50, offset = 0 } = options;
|
|
26507
25100
|
const [ok, err, transitions] = await tryFn(
|
|
26508
|
-
() => this.database.
|
|
25101
|
+
() => this.database.resources[this.config.transitionLogResource].query({
|
|
26509
25102
|
machineId,
|
|
26510
25103
|
entityId
|
|
26511
25104
|
}, {
|
|
@@ -26547,7 +25140,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
26547
25140
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26548
25141
|
const stateId = `${machineId}_${entityId}`;
|
|
26549
25142
|
const [ok, err] = await tryFn(
|
|
26550
|
-
() => this.database.
|
|
25143
|
+
() => this.database.resources[this.config.stateResource].insert({
|
|
26551
25144
|
id: stateId,
|
|
26552
25145
|
machineId,
|
|
26553
25146
|
entityId,
|
|
@@ -26636,9 +25229,6 @@ class StateMachinePlugin extends Plugin {
|
|
|
26636
25229
|
}
|
|
26637
25230
|
async stop() {
|
|
26638
25231
|
this.machines.clear();
|
|
26639
|
-
}
|
|
26640
|
-
async cleanup() {
|
|
26641
|
-
await this.stop();
|
|
26642
25232
|
this.removeAllListeners();
|
|
26643
25233
|
}
|
|
26644
25234
|
}
|
|
@@ -26653,7 +25243,7 @@ class TfStateError extends Error {
|
|
|
26653
25243
|
}
|
|
26654
25244
|
class InvalidStateFileError extends TfStateError {
|
|
26655
25245
|
constructor(filePath, reason, context = {}) {
|
|
26656
|
-
super(`Invalid
|
|
25246
|
+
super(`Invalid Tfstate file "${filePath}": ${reason}`, context);
|
|
26657
25247
|
this.name = "InvalidStateFileError";
|
|
26658
25248
|
this.filePath = filePath;
|
|
26659
25249
|
this.reason = reason;
|
|
@@ -26662,7 +25252,7 @@ class InvalidStateFileError extends TfStateError {
|
|
|
26662
25252
|
class UnsupportedStateVersionError extends TfStateError {
|
|
26663
25253
|
constructor(version, supportedVersions, context = {}) {
|
|
26664
25254
|
super(
|
|
26665
|
-
`
|
|
25255
|
+
`Tfstate version ${version} is not supported. Supported versions: ${supportedVersions.join(", ")}`,
|
|
26666
25256
|
context
|
|
26667
25257
|
);
|
|
26668
25258
|
this.name = "UnsupportedStateVersionError";
|
|
@@ -26672,7 +25262,7 @@ class UnsupportedStateVersionError extends TfStateError {
|
|
|
26672
25262
|
}
|
|
26673
25263
|
class StateFileNotFoundError extends TfStateError {
|
|
26674
25264
|
constructor(filePath, context = {}) {
|
|
26675
|
-
super(`
|
|
25265
|
+
super(`Tfstate file not found: ${filePath}`, context);
|
|
26676
25266
|
this.name = "StateFileNotFoundError";
|
|
26677
25267
|
this.filePath = filePath;
|
|
26678
25268
|
}
|
|
@@ -34895,42 +33485,23 @@ class FilesystemTfStateDriver extends TfStateDriver {
|
|
|
34895
33485
|
class TfStatePlugin extends Plugin {
|
|
34896
33486
|
constructor(config = {}) {
|
|
34897
33487
|
super(config);
|
|
34898
|
-
|
|
34899
|
-
|
|
34900
|
-
|
|
34901
|
-
|
|
34902
|
-
|
|
34903
|
-
|
|
34904
|
-
|
|
34905
|
-
|
|
34906
|
-
|
|
34907
|
-
|
|
34908
|
-
|
|
34909
|
-
|
|
34910
|
-
|
|
34911
|
-
|
|
34912
|
-
|
|
34913
|
-
|
|
34914
|
-
|
|
34915
|
-
this.filters = config.filters || {};
|
|
34916
|
-
this.verbose = config.verbose || false;
|
|
34917
|
-
} else {
|
|
34918
|
-
this.driverType = null;
|
|
34919
|
-
this.driverConfig = {};
|
|
34920
|
-
this.resourceName = config.resourceName || "plg_tfstate_resources";
|
|
34921
|
-
this.stateFilesName = config.stateFilesName || "plg_tfstate_state_files";
|
|
34922
|
-
this.diffsName = config.diffsName || config.stateHistoryName || "plg_tfstate_state_diffs";
|
|
34923
|
-
this.stateHistoryName = this.diffsName;
|
|
34924
|
-
this.autoSync = config.autoSync || false;
|
|
34925
|
-
this.watchPaths = Array.isArray(config.watchPaths) ? config.watchPaths : [];
|
|
34926
|
-
this.filters = config.filters || {};
|
|
34927
|
-
this.trackDiffs = config.trackDiffs !== void 0 ? config.trackDiffs : true;
|
|
34928
|
-
this.diffsLookback = 10;
|
|
34929
|
-
this.asyncPartitions = config.asyncPartitions !== void 0 ? config.asyncPartitions : true;
|
|
34930
|
-
this.verbose = config.verbose || false;
|
|
34931
|
-
this.monitorEnabled = false;
|
|
34932
|
-
this.monitorCron = "*/5 * * * *";
|
|
34933
|
-
}
|
|
33488
|
+
this.driverType = config.driver || null;
|
|
33489
|
+
this.driverConfig = config.config || {};
|
|
33490
|
+
const resources = config.resources || {};
|
|
33491
|
+
this.resourceName = resources.resources || config.resourceName || "plg_tfstate_resources";
|
|
33492
|
+
this.stateFilesName = resources.stateFiles || config.stateFilesName || "plg_tfstate_state_files";
|
|
33493
|
+
this.diffsName = resources.diffs || config.diffsName || "plg_tfstate_state_diffs";
|
|
33494
|
+
const monitor = config.monitor || {};
|
|
33495
|
+
this.monitorEnabled = monitor.enabled || false;
|
|
33496
|
+
this.monitorCron = monitor.cron || "*/5 * * * *";
|
|
33497
|
+
const diffs = config.diffs || {};
|
|
33498
|
+
this.trackDiffs = diffs.enabled !== void 0 ? diffs.enabled : config.trackDiffs !== void 0 ? config.trackDiffs : true;
|
|
33499
|
+
this.diffsLookback = diffs.lookback || 10;
|
|
33500
|
+
this.asyncPartitions = config.asyncPartitions !== void 0 ? config.asyncPartitions : true;
|
|
33501
|
+
this.autoSync = config.autoSync || false;
|
|
33502
|
+
this.watchPaths = config.watchPaths || [];
|
|
33503
|
+
this.filters = config.filters || {};
|
|
33504
|
+
this.verbose = config.verbose || false;
|
|
34934
33505
|
this.supportedVersions = [3, 4];
|
|
34935
33506
|
this.driver = null;
|
|
34936
33507
|
this.resource = null;
|
|
@@ -34980,7 +33551,7 @@ class TfStatePlugin extends Plugin {
|
|
|
34980
33551
|
name: this.lineagesName,
|
|
34981
33552
|
attributes: {
|
|
34982
33553
|
id: "string|required",
|
|
34983
|
-
// = lineage UUID from
|
|
33554
|
+
// = lineage UUID from Tfstate
|
|
34984
33555
|
latestSerial: "number",
|
|
34985
33556
|
// Track latest for quick access
|
|
34986
33557
|
latestStateId: "string",
|
|
@@ -35693,7 +34264,7 @@ class TfStatePlugin extends Plugin {
|
|
|
35693
34264
|
return result;
|
|
35694
34265
|
}
|
|
35695
34266
|
/**
|
|
35696
|
-
* Read and parse
|
|
34267
|
+
* Read and parse Tfstate file
|
|
35697
34268
|
* @private
|
|
35698
34269
|
*/
|
|
35699
34270
|
async _readStateFile(filePath) {
|
|
@@ -35730,7 +34301,7 @@ class TfStatePlugin extends Plugin {
|
|
|
35730
34301
|
}
|
|
35731
34302
|
}
|
|
35732
34303
|
/**
|
|
35733
|
-
* Validate
|
|
34304
|
+
* Validate Tfstate version
|
|
35734
34305
|
* @private
|
|
35735
34306
|
*/
|
|
35736
34307
|
_validateStateVersion(state) {
|
|
@@ -35743,7 +34314,7 @@ class TfStatePlugin extends Plugin {
|
|
|
35743
34314
|
}
|
|
35744
34315
|
}
|
|
35745
34316
|
/**
|
|
35746
|
-
* Extract resources from
|
|
34317
|
+
* Extract resources from Tfstate
|
|
35747
34318
|
* @private
|
|
35748
34319
|
*/
|
|
35749
34320
|
async _extractResources(state, filePath, stateFileId, lineageId) {
|
|
@@ -36310,14 +34881,14 @@ class TfStatePlugin extends Plugin {
|
|
|
36310
34881
|
}
|
|
36311
34882
|
}
|
|
36312
34883
|
/**
|
|
36313
|
-
* Export resources to
|
|
34884
|
+
* Export resources to Tfstate format
|
|
36314
34885
|
* @param {Object} options - Export options
|
|
36315
34886
|
* @param {number} options.serial - Specific serial to export (default: latest)
|
|
36316
34887
|
* @param {string[]} options.resourceTypes - Filter by resource types
|
|
36317
34888
|
* @param {string} options.terraformVersion - Terraform version for output (default: '1.5.0')
|
|
36318
34889
|
* @param {string} options.lineage - State lineage (default: auto-generated)
|
|
36319
34890
|
* @param {Object} options.outputs - Terraform outputs to include
|
|
36320
|
-
* @returns {Promise<Object>}
|
|
34891
|
+
* @returns {Promise<Object>} Tfstate object
|
|
36321
34892
|
*
|
|
36322
34893
|
* @example
|
|
36323
34894
|
* // Export latest state
|
|
@@ -37667,6 +36238,533 @@ class VectorPlugin extends Plugin {
|
|
|
37667
36238
|
}
|
|
37668
36239
|
}
|
|
37669
36240
|
|
|
36241
|
+
function mapFieldTypeToTypeScript(fieldType) {
|
|
36242
|
+
const baseType = fieldType.split("|")[0].trim();
|
|
36243
|
+
const typeMap = {
|
|
36244
|
+
"string": "string",
|
|
36245
|
+
"number": "number",
|
|
36246
|
+
"integer": "number",
|
|
36247
|
+
"boolean": "boolean",
|
|
36248
|
+
"array": "any[]",
|
|
36249
|
+
"object": "Record<string, any>",
|
|
36250
|
+
"json": "Record<string, any>",
|
|
36251
|
+
"secret": "string",
|
|
36252
|
+
"email": "string",
|
|
36253
|
+
"url": "string",
|
|
36254
|
+
"date": "string",
|
|
36255
|
+
// ISO date string
|
|
36256
|
+
"datetime": "string",
|
|
36257
|
+
// ISO datetime string
|
|
36258
|
+
"ip4": "string",
|
|
36259
|
+
"ip6": "string"
|
|
36260
|
+
};
|
|
36261
|
+
if (baseType.startsWith("embedding:")) {
|
|
36262
|
+
const dimensions = parseInt(baseType.split(":")[1]);
|
|
36263
|
+
return `number[] /* ${dimensions} dimensions */`;
|
|
36264
|
+
}
|
|
36265
|
+
return typeMap[baseType] || "any";
|
|
36266
|
+
}
|
|
36267
|
+
function isFieldRequired(fieldDef) {
|
|
36268
|
+
if (typeof fieldDef === "string") {
|
|
36269
|
+
return fieldDef.includes("|required");
|
|
36270
|
+
}
|
|
36271
|
+
if (typeof fieldDef === "object" && fieldDef.required) {
|
|
36272
|
+
return true;
|
|
36273
|
+
}
|
|
36274
|
+
return false;
|
|
36275
|
+
}
|
|
36276
|
+
function generateResourceInterface(resourceName, attributes, timestamps = false) {
|
|
36277
|
+
const interfaceName = toPascalCase(resourceName);
|
|
36278
|
+
const lines = [];
|
|
36279
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
36280
|
+
lines.push(` /** Resource ID (auto-generated) */`);
|
|
36281
|
+
lines.push(` id: string;`);
|
|
36282
|
+
lines.push("");
|
|
36283
|
+
for (const [fieldName, fieldDef] of Object.entries(attributes)) {
|
|
36284
|
+
const required = isFieldRequired(fieldDef);
|
|
36285
|
+
const optional = required ? "" : "?";
|
|
36286
|
+
let tsType;
|
|
36287
|
+
if (typeof fieldDef === "string") {
|
|
36288
|
+
tsType = mapFieldTypeToTypeScript(fieldDef);
|
|
36289
|
+
} else if (typeof fieldDef === "object" && fieldDef.type) {
|
|
36290
|
+
tsType = mapFieldTypeToTypeScript(fieldDef.type);
|
|
36291
|
+
if (fieldDef.type === "object" && fieldDef.props) {
|
|
36292
|
+
tsType = "{\n";
|
|
36293
|
+
for (const [propName, propDef] of Object.entries(fieldDef.props)) {
|
|
36294
|
+
const propType = typeof propDef === "string" ? mapFieldTypeToTypeScript(propDef) : mapFieldTypeToTypeScript(propDef.type);
|
|
36295
|
+
const propRequired = isFieldRequired(propDef);
|
|
36296
|
+
tsType += ` ${propName}${propRequired ? "" : "?"}: ${propType};
|
|
36297
|
+
`;
|
|
36298
|
+
}
|
|
36299
|
+
tsType += " }";
|
|
36300
|
+
}
|
|
36301
|
+
if (fieldDef.type === "array" && fieldDef.items) {
|
|
36302
|
+
const itemType = mapFieldTypeToTypeScript(fieldDef.items);
|
|
36303
|
+
tsType = `Array<${itemType}>`;
|
|
36304
|
+
}
|
|
36305
|
+
} else {
|
|
36306
|
+
tsType = "any";
|
|
36307
|
+
}
|
|
36308
|
+
if (fieldDef.description) {
|
|
36309
|
+
lines.push(` /** ${fieldDef.description} */`);
|
|
36310
|
+
}
|
|
36311
|
+
lines.push(` ${fieldName}${optional}: ${tsType};`);
|
|
36312
|
+
}
|
|
36313
|
+
if (timestamps) {
|
|
36314
|
+
lines.push("");
|
|
36315
|
+
lines.push(` /** Creation timestamp (ISO 8601) */`);
|
|
36316
|
+
lines.push(` createdAt: string;`);
|
|
36317
|
+
lines.push(` /** Last update timestamp (ISO 8601) */`);
|
|
36318
|
+
lines.push(` updatedAt: string;`);
|
|
36319
|
+
}
|
|
36320
|
+
lines.push("}");
|
|
36321
|
+
lines.push("");
|
|
36322
|
+
return lines.join("\n");
|
|
36323
|
+
}
|
|
36324
|
+
function toPascalCase(str) {
|
|
36325
|
+
return str.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
36326
|
+
}
|
|
36327
|
+
async function generateTypes(database, options = {}) {
|
|
36328
|
+
const {
|
|
36329
|
+
outputPath = "./types/database.d.ts",
|
|
36330
|
+
moduleName = "s3db.js",
|
|
36331
|
+
includeResource = true
|
|
36332
|
+
} = options;
|
|
36333
|
+
const lines = [];
|
|
36334
|
+
lines.push("/**");
|
|
36335
|
+
lines.push(" * Auto-generated TypeScript definitions for s3db.js resources");
|
|
36336
|
+
lines.push(" * Generated at: " + (/* @__PURE__ */ new Date()).toISOString());
|
|
36337
|
+
lines.push(" * DO NOT EDIT - This file is auto-generated");
|
|
36338
|
+
lines.push(" */");
|
|
36339
|
+
lines.push("");
|
|
36340
|
+
if (includeResource) {
|
|
36341
|
+
lines.push(`import { Resource, Database } from '${moduleName}';`);
|
|
36342
|
+
lines.push("");
|
|
36343
|
+
}
|
|
36344
|
+
const resourceInterfaces = [];
|
|
36345
|
+
for (const [name, resource] of Object.entries(database.resources)) {
|
|
36346
|
+
const attributes = resource.config?.attributes || resource.attributes || {};
|
|
36347
|
+
const timestamps = resource.config?.timestamps || false;
|
|
36348
|
+
const interfaceDef = generateResourceInterface(name, attributes, timestamps);
|
|
36349
|
+
lines.push(interfaceDef);
|
|
36350
|
+
resourceInterfaces.push({
|
|
36351
|
+
name,
|
|
36352
|
+
interfaceName: toPascalCase(name),
|
|
36353
|
+
resource
|
|
36354
|
+
});
|
|
36355
|
+
}
|
|
36356
|
+
lines.push("/**");
|
|
36357
|
+
lines.push(" * Typed resource map for property access");
|
|
36358
|
+
lines.push(" * @example");
|
|
36359
|
+
lines.push(" * const users = db.resources.users; // Type-safe!");
|
|
36360
|
+
lines.push(' * const user = await users.get("id"); // Autocomplete works!');
|
|
36361
|
+
lines.push(" */");
|
|
36362
|
+
lines.push("export interface ResourceMap {");
|
|
36363
|
+
for (const { name, interfaceName } of resourceInterfaces) {
|
|
36364
|
+
lines.push(` /** ${interfaceName} resource */`);
|
|
36365
|
+
if (includeResource) {
|
|
36366
|
+
lines.push(` ${name}: Resource<${interfaceName}>;`);
|
|
36367
|
+
} else {
|
|
36368
|
+
lines.push(` ${name}: any;`);
|
|
36369
|
+
}
|
|
36370
|
+
}
|
|
36371
|
+
lines.push("}");
|
|
36372
|
+
lines.push("");
|
|
36373
|
+
if (includeResource) {
|
|
36374
|
+
lines.push("/**");
|
|
36375
|
+
lines.push(" * Extended Database class with typed resources");
|
|
36376
|
+
lines.push(" */");
|
|
36377
|
+
lines.push("declare module 's3db.js' {");
|
|
36378
|
+
lines.push(" interface Database {");
|
|
36379
|
+
lines.push(" resources: ResourceMap;");
|
|
36380
|
+
lines.push(" }");
|
|
36381
|
+
lines.push("");
|
|
36382
|
+
lines.push(" interface Resource<T = any> {");
|
|
36383
|
+
lines.push(" get(id: string): Promise<T>;");
|
|
36384
|
+
lines.push(" getOrNull(id: string): Promise<T | null>;");
|
|
36385
|
+
lines.push(" getOrThrow(id: string): Promise<T>;");
|
|
36386
|
+
lines.push(" insert(data: Partial<T>): Promise<T>;");
|
|
36387
|
+
lines.push(" update(id: string, data: Partial<T>): Promise<T>;");
|
|
36388
|
+
lines.push(" patch(id: string, data: Partial<T>): Promise<T>;");
|
|
36389
|
+
lines.push(" replace(id: string, data: Partial<T>): Promise<T>;");
|
|
36390
|
+
lines.push(" delete(id: string): Promise<void>;");
|
|
36391
|
+
lines.push(" list(options?: any): Promise<T[]>;");
|
|
36392
|
+
lines.push(" query(filters: Partial<T>, options?: any): Promise<T[]>;");
|
|
36393
|
+
lines.push(" validate(data: Partial<T>, options?: any): Promise<{ valid: boolean; errors: any[]; data: T | null }>;");
|
|
36394
|
+
lines.push(" }");
|
|
36395
|
+
lines.push("}");
|
|
36396
|
+
}
|
|
36397
|
+
const content = lines.join("\n");
|
|
36398
|
+
if (outputPath) {
|
|
36399
|
+
await promises.mkdir(path$1.dirname(outputPath), { recursive: true });
|
|
36400
|
+
await promises.writeFile(outputPath, content, "utf-8");
|
|
36401
|
+
}
|
|
36402
|
+
return content;
|
|
36403
|
+
}
|
|
36404
|
+
async function printTypes(database, options = {}) {
|
|
36405
|
+
const types = await generateTypes(database, { ...options, outputPath: null });
|
|
36406
|
+
console.log(types);
|
|
36407
|
+
return types;
|
|
36408
|
+
}
|
|
36409
|
+
|
|
36410
|
+
class Factory {
|
|
36411
|
+
/**
|
|
36412
|
+
* Global sequence counter
|
|
36413
|
+
* @private
|
|
36414
|
+
*/
|
|
36415
|
+
static _sequences = /* @__PURE__ */ new Map();
|
|
36416
|
+
/**
|
|
36417
|
+
* Registered factories
|
|
36418
|
+
* @private
|
|
36419
|
+
*/
|
|
36420
|
+
static _factories = /* @__PURE__ */ new Map();
|
|
36421
|
+
/**
|
|
36422
|
+
* Database instance (set globally)
|
|
36423
|
+
* @private
|
|
36424
|
+
*/
|
|
36425
|
+
static _database = null;
|
|
36426
|
+
/**
|
|
36427
|
+
* Create a new factory definition
|
|
36428
|
+
* @param {string} resourceName - Resource name
|
|
36429
|
+
* @param {Object|Function} definition - Field definitions or function
|
|
36430
|
+
* @param {Object} options - Factory options
|
|
36431
|
+
* @returns {Factory} Factory instance
|
|
36432
|
+
*/
|
|
36433
|
+
static define(resourceName, definition, options = {}) {
|
|
36434
|
+
const factory = new Factory(resourceName, definition, options);
|
|
36435
|
+
Factory._factories.set(resourceName, factory);
|
|
36436
|
+
return factory;
|
|
36437
|
+
}
|
|
36438
|
+
/**
|
|
36439
|
+
* Set global database instance
|
|
36440
|
+
* @param {Database} database - s3db.js Database instance
|
|
36441
|
+
*/
|
|
36442
|
+
static setDatabase(database) {
|
|
36443
|
+
Factory._database = database;
|
|
36444
|
+
}
|
|
36445
|
+
/**
|
|
36446
|
+
* Get factory by resource name
|
|
36447
|
+
* @param {string} resourceName - Resource name
|
|
36448
|
+
* @returns {Factory} Factory instance
|
|
36449
|
+
*/
|
|
36450
|
+
static get(resourceName) {
|
|
36451
|
+
return Factory._factories.get(resourceName);
|
|
36452
|
+
}
|
|
36453
|
+
/**
|
|
36454
|
+
* Reset all sequences
|
|
36455
|
+
*/
|
|
36456
|
+
static resetSequences() {
|
|
36457
|
+
Factory._sequences.clear();
|
|
36458
|
+
}
|
|
36459
|
+
/**
|
|
36460
|
+
* Reset all factories
|
|
36461
|
+
*/
|
|
36462
|
+
static reset() {
|
|
36463
|
+
Factory._sequences.clear();
|
|
36464
|
+
Factory._factories.clear();
|
|
36465
|
+
Factory._database = null;
|
|
36466
|
+
}
|
|
36467
|
+
/**
|
|
36468
|
+
* Constructor
|
|
36469
|
+
* @param {string} resourceName - Resource name
|
|
36470
|
+
* @param {Object|Function} definition - Field definitions
|
|
36471
|
+
* @param {Object} options - Factory options
|
|
36472
|
+
*/
|
|
36473
|
+
constructor(resourceName, definition, options = {}) {
|
|
36474
|
+
this.resourceName = resourceName;
|
|
36475
|
+
this.definition = definition;
|
|
36476
|
+
this.options = options;
|
|
36477
|
+
this.traits = /* @__PURE__ */ new Map();
|
|
36478
|
+
this.afterCreateCallbacks = [];
|
|
36479
|
+
this.beforeCreateCallbacks = [];
|
|
36480
|
+
}
|
|
36481
|
+
/**
|
|
36482
|
+
* Get next sequence number
|
|
36483
|
+
* @param {string} name - Sequence name (default: factory name)
|
|
36484
|
+
* @returns {number} Next sequence number
|
|
36485
|
+
*/
|
|
36486
|
+
sequence(name = this.resourceName) {
|
|
36487
|
+
const current = Factory._sequences.get(name) || 0;
|
|
36488
|
+
const next = current + 1;
|
|
36489
|
+
Factory._sequences.set(name, next);
|
|
36490
|
+
return next;
|
|
36491
|
+
}
|
|
36492
|
+
/**
|
|
36493
|
+
* Define a trait (state variation)
|
|
36494
|
+
* @param {string} name - Trait name
|
|
36495
|
+
* @param {Object|Function} attributes - Trait attributes
|
|
36496
|
+
* @returns {Factory} This factory (for chaining)
|
|
36497
|
+
*/
|
|
36498
|
+
trait(name, attributes) {
|
|
36499
|
+
this.traits.set(name, attributes);
|
|
36500
|
+
return this;
|
|
36501
|
+
}
|
|
36502
|
+
/**
|
|
36503
|
+
* Register after create callback
|
|
36504
|
+
* @param {Function} callback - Callback function
|
|
36505
|
+
* @returns {Factory} This factory (for chaining)
|
|
36506
|
+
*/
|
|
36507
|
+
afterCreate(callback) {
|
|
36508
|
+
this.afterCreateCallbacks.push(callback);
|
|
36509
|
+
return this;
|
|
36510
|
+
}
|
|
36511
|
+
/**
|
|
36512
|
+
* Register before create callback
|
|
36513
|
+
* @param {Function} callback - Callback function
|
|
36514
|
+
* @returns {Factory} This factory (for chaining)
|
|
36515
|
+
*/
|
|
36516
|
+
beforeCreate(callback) {
|
|
36517
|
+
this.beforeCreateCallbacks.push(callback);
|
|
36518
|
+
return this;
|
|
36519
|
+
}
|
|
36520
|
+
/**
|
|
36521
|
+
* Build attributes without creating in database
|
|
36522
|
+
* @param {Object} overrides - Override attributes
|
|
36523
|
+
* @param {Object} options - Build options
|
|
36524
|
+
* @returns {Promise<Object>} Built attributes
|
|
36525
|
+
*/
|
|
36526
|
+
async build(overrides = {}, options = {}) {
|
|
36527
|
+
const { traits = [] } = options;
|
|
36528
|
+
const seq = this.sequence();
|
|
36529
|
+
let attributes = typeof this.definition === "function" ? await this.definition({ seq, factory: this }) : { ...this.definition };
|
|
36530
|
+
for (const traitName of traits) {
|
|
36531
|
+
const trait = this.traits.get(traitName);
|
|
36532
|
+
if (!trait) {
|
|
36533
|
+
throw new Error(`Trait '${traitName}' not found in factory '${this.resourceName}'`);
|
|
36534
|
+
}
|
|
36535
|
+
const traitAttrs = typeof trait === "function" ? await trait({ seq, factory: this }) : trait;
|
|
36536
|
+
attributes = { ...attributes, ...traitAttrs };
|
|
36537
|
+
}
|
|
36538
|
+
attributes = { ...attributes, ...overrides };
|
|
36539
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
36540
|
+
if (typeof value === "function") {
|
|
36541
|
+
attributes[key] = await value({ seq, factory: this });
|
|
36542
|
+
}
|
|
36543
|
+
}
|
|
36544
|
+
return attributes;
|
|
36545
|
+
}
|
|
36546
|
+
/**
|
|
36547
|
+
* Create resource in database
|
|
36548
|
+
* @param {Object} overrides - Override attributes
|
|
36549
|
+
* @param {Object} options - Create options
|
|
36550
|
+
* @returns {Promise<Object>} Created resource
|
|
36551
|
+
*/
|
|
36552
|
+
async create(overrides = {}, options = {}) {
|
|
36553
|
+
const { database = Factory._database } = options;
|
|
36554
|
+
if (!database) {
|
|
36555
|
+
throw new Error("Database not set. Use Factory.setDatabase(db) or pass database option");
|
|
36556
|
+
}
|
|
36557
|
+
let attributes = await this.build(overrides, options);
|
|
36558
|
+
for (const callback of this.beforeCreateCallbacks) {
|
|
36559
|
+
attributes = await callback(attributes) || attributes;
|
|
36560
|
+
}
|
|
36561
|
+
const resource = database.resources[this.resourceName];
|
|
36562
|
+
if (!resource) {
|
|
36563
|
+
throw new Error(`Resource '${this.resourceName}' not found in database`);
|
|
36564
|
+
}
|
|
36565
|
+
let created = await resource.insert(attributes);
|
|
36566
|
+
for (const callback of this.afterCreateCallbacks) {
|
|
36567
|
+
created = await callback(created, { database }) || created;
|
|
36568
|
+
}
|
|
36569
|
+
return created;
|
|
36570
|
+
}
|
|
36571
|
+
/**
|
|
36572
|
+
* Create multiple resources
|
|
36573
|
+
* @param {number} count - Number of resources to create
|
|
36574
|
+
* @param {Object} overrides - Override attributes
|
|
36575
|
+
* @param {Object} options - Create options
|
|
36576
|
+
* @returns {Promise<Object[]>} Created resources
|
|
36577
|
+
*/
|
|
36578
|
+
async createMany(count, overrides = {}, options = {}) {
|
|
36579
|
+
const resources = [];
|
|
36580
|
+
for (let i = 0; i < count; i++) {
|
|
36581
|
+
const resource = await this.create(overrides, options);
|
|
36582
|
+
resources.push(resource);
|
|
36583
|
+
}
|
|
36584
|
+
return resources;
|
|
36585
|
+
}
|
|
36586
|
+
/**
|
|
36587
|
+
* Build multiple resources without creating
|
|
36588
|
+
* @param {number} count - Number of resources to build
|
|
36589
|
+
* @param {Object} overrides - Override attributes
|
|
36590
|
+
* @param {Object} options - Build options
|
|
36591
|
+
* @returns {Promise<Object[]>} Built resources
|
|
36592
|
+
*/
|
|
36593
|
+
async buildMany(count, overrides = {}, options = {}) {
|
|
36594
|
+
const resources = [];
|
|
36595
|
+
for (let i = 0; i < count; i++) {
|
|
36596
|
+
const resource = await this.build(overrides, options);
|
|
36597
|
+
resources.push(resource);
|
|
36598
|
+
}
|
|
36599
|
+
return resources;
|
|
36600
|
+
}
|
|
36601
|
+
/**
|
|
36602
|
+
* Create with specific traits
|
|
36603
|
+
* @param {string|string[]} traits - Trait name(s)
|
|
36604
|
+
* @param {Object} overrides - Override attributes
|
|
36605
|
+
* @param {Object} options - Create options
|
|
36606
|
+
* @returns {Promise<Object>} Created resource
|
|
36607
|
+
*/
|
|
36608
|
+
async createWithTraits(traits, overrides = {}, options = {}) {
|
|
36609
|
+
const traitArray = Array.isArray(traits) ? traits : [traits];
|
|
36610
|
+
return this.create(overrides, { ...options, traits: traitArray });
|
|
36611
|
+
}
|
|
36612
|
+
/**
|
|
36613
|
+
* Build with specific traits
|
|
36614
|
+
* @param {string|string[]} traits - Trait name(s)
|
|
36615
|
+
* @param {Object} overrides - Override attributes
|
|
36616
|
+
* @param {Object} options - Build options
|
|
36617
|
+
* @returns {Promise<Object>} Built resource
|
|
36618
|
+
*/
|
|
36619
|
+
async buildWithTraits(traits, overrides = {}, options = {}) {
|
|
36620
|
+
const traitArray = Array.isArray(traits) ? traits : [traits];
|
|
36621
|
+
return this.build(overrides, { ...options, traits: traitArray });
|
|
36622
|
+
}
|
|
36623
|
+
}
|
|
36624
|
+
|
|
36625
|
+
class Seeder {
|
|
36626
|
+
/**
|
|
36627
|
+
* Constructor
|
|
36628
|
+
* @param {Database} database - s3db.js Database instance
|
|
36629
|
+
* @param {Object} options - Seeder options
|
|
36630
|
+
*/
|
|
36631
|
+
constructor(database, options = {}) {
|
|
36632
|
+
this.database = database;
|
|
36633
|
+
this.options = options;
|
|
36634
|
+
this.verbose = options.verbose !== false;
|
|
36635
|
+
}
|
|
36636
|
+
/**
|
|
36637
|
+
* Log message (if verbose)
|
|
36638
|
+
* @param {string} message - Message to log
|
|
36639
|
+
* @private
|
|
36640
|
+
*/
|
|
36641
|
+
log(message) {
|
|
36642
|
+
if (this.verbose) {
|
|
36643
|
+
console.log(`[Seeder] ${message}`);
|
|
36644
|
+
}
|
|
36645
|
+
}
|
|
36646
|
+
/**
|
|
36647
|
+
* Seed resources using factories
|
|
36648
|
+
* @param {Object} specs - Seed specifications { resourceName: count }
|
|
36649
|
+
* @returns {Promise<Object>} Created resources by resource name
|
|
36650
|
+
*
|
|
36651
|
+
* @example
|
|
36652
|
+
* const created = await seeder.seed({
|
|
36653
|
+
* users: 10,
|
|
36654
|
+
* posts: 50
|
|
36655
|
+
* });
|
|
36656
|
+
*/
|
|
36657
|
+
async seed(specs) {
|
|
36658
|
+
const created = {};
|
|
36659
|
+
for (const [resourceName, count] of Object.entries(specs)) {
|
|
36660
|
+
this.log(`Seeding ${count} ${resourceName}...`);
|
|
36661
|
+
const factory = Factory.get(resourceName);
|
|
36662
|
+
if (!factory) {
|
|
36663
|
+
throw new Error(`Factory for '${resourceName}' not found. Define it with Factory.define()`);
|
|
36664
|
+
}
|
|
36665
|
+
created[resourceName] = await factory.createMany(count, {}, { database: this.database });
|
|
36666
|
+
this.log(`\u2705 Created ${count} ${resourceName}`);
|
|
36667
|
+
}
|
|
36668
|
+
return created;
|
|
36669
|
+
}
|
|
36670
|
+
/**
|
|
36671
|
+
* Seed with custom callback
|
|
36672
|
+
* @param {Function} callback - Seeding callback
|
|
36673
|
+
* @returns {Promise<any>} Result of callback
|
|
36674
|
+
*
|
|
36675
|
+
* @example
|
|
36676
|
+
* await seeder.call(async (db) => {
|
|
36677
|
+
* const user = await UserFactory.create();
|
|
36678
|
+
* const posts = await PostFactory.createMany(5, { userId: user.id });
|
|
36679
|
+
* return { user, posts };
|
|
36680
|
+
* });
|
|
36681
|
+
*/
|
|
36682
|
+
async call(callback) {
|
|
36683
|
+
this.log("Running custom seeder...");
|
|
36684
|
+
const result = await callback(this.database);
|
|
36685
|
+
this.log("\u2705 Custom seeder completed");
|
|
36686
|
+
return result;
|
|
36687
|
+
}
|
|
36688
|
+
/**
|
|
36689
|
+
* Truncate resources (delete all data)
|
|
36690
|
+
* @param {string[]} resourceNames - Resource names to truncate
|
|
36691
|
+
* @returns {Promise<void>}
|
|
36692
|
+
*
|
|
36693
|
+
* @example
|
|
36694
|
+
* await seeder.truncate(['users', 'posts']);
|
|
36695
|
+
*/
|
|
36696
|
+
async truncate(resourceNames) {
|
|
36697
|
+
for (const resourceName of resourceNames) {
|
|
36698
|
+
this.log(`Truncating ${resourceName}...`);
|
|
36699
|
+
const resource = this.database.resources[resourceName];
|
|
36700
|
+
if (!resource) {
|
|
36701
|
+
this.log(`\u26A0\uFE0F Resource '${resourceName}' not found, skipping`);
|
|
36702
|
+
continue;
|
|
36703
|
+
}
|
|
36704
|
+
const ids = await resource.listIds();
|
|
36705
|
+
if (ids.length > 0) {
|
|
36706
|
+
await resource.deleteMany(ids);
|
|
36707
|
+
this.log(`\u2705 Deleted ${ids.length} ${resourceName}`);
|
|
36708
|
+
} else {
|
|
36709
|
+
this.log(`\u2705 ${resourceName} already empty`);
|
|
36710
|
+
}
|
|
36711
|
+
}
|
|
36712
|
+
}
|
|
36713
|
+
/**
|
|
36714
|
+
* Truncate all resources
|
|
36715
|
+
* @returns {Promise<void>}
|
|
36716
|
+
*/
|
|
36717
|
+
async truncateAll() {
|
|
36718
|
+
const resourceNames = Object.keys(this.database.resources);
|
|
36719
|
+
await this.truncate(resourceNames);
|
|
36720
|
+
}
|
|
36721
|
+
/**
|
|
36722
|
+
* Run multiple seeders in order
|
|
36723
|
+
* @param {Function[]} seeders - Array of seeder functions
|
|
36724
|
+
* @returns {Promise<Object[]>} Results of each seeder
|
|
36725
|
+
*
|
|
36726
|
+
* @example
|
|
36727
|
+
* await seeder.run([
|
|
36728
|
+
* async (db) => await UserFactory.createMany(10),
|
|
36729
|
+
* async (db) => await PostFactory.createMany(50)
|
|
36730
|
+
* ]);
|
|
36731
|
+
*/
|
|
36732
|
+
async run(seeders) {
|
|
36733
|
+
const results = [];
|
|
36734
|
+
for (const seederFn of seeders) {
|
|
36735
|
+
this.log(`Running seeder ${seederFn.name || "anonymous"}...`);
|
|
36736
|
+
const result = await seederFn(this.database);
|
|
36737
|
+
results.push(result);
|
|
36738
|
+
this.log(`\u2705 Completed ${seederFn.name || "anonymous"}`);
|
|
36739
|
+
}
|
|
36740
|
+
return results;
|
|
36741
|
+
}
|
|
36742
|
+
/**
|
|
36743
|
+
* Seed and return specific resources
|
|
36744
|
+
* @param {Object} specs - Seed specifications
|
|
36745
|
+
* @returns {Promise<Object>} Created resources
|
|
36746
|
+
*
|
|
36747
|
+
* @example
|
|
36748
|
+
* const { users, posts } = await seeder.seedAndReturn({
|
|
36749
|
+
* users: 5,
|
|
36750
|
+
* posts: 10
|
|
36751
|
+
* });
|
|
36752
|
+
*/
|
|
36753
|
+
async seedAndReturn(specs) {
|
|
36754
|
+
return await this.seed(specs);
|
|
36755
|
+
}
|
|
36756
|
+
/**
|
|
36757
|
+
* Reset database (truncate all and reset sequences)
|
|
36758
|
+
* @returns {Promise<void>}
|
|
36759
|
+
*/
|
|
36760
|
+
async reset() {
|
|
36761
|
+
this.log("Resetting database...");
|
|
36762
|
+
await this.truncateAll();
|
|
36763
|
+
Factory.resetSequences();
|
|
36764
|
+
this.log("\u2705 Database reset complete");
|
|
36765
|
+
}
|
|
36766
|
+
}
|
|
36767
|
+
|
|
37670
36768
|
function sanitizeLabel(value) {
|
|
37671
36769
|
if (typeof value !== "string") {
|
|
37672
36770
|
value = String(value);
|
|
@@ -38068,6 +37166,7 @@ exports.DynamoDBReplicator = DynamoDBReplicator;
|
|
|
38068
37166
|
exports.EncryptionError = EncryptionError;
|
|
38069
37167
|
exports.ErrorMap = ErrorMap;
|
|
38070
37168
|
exports.EventualConsistencyPlugin = EventualConsistencyPlugin;
|
|
37169
|
+
exports.Factory = Factory;
|
|
38071
37170
|
exports.FilesystemBackupDriver = FilesystemBackupDriver;
|
|
38072
37171
|
exports.FilesystemCache = FilesystemCache;
|
|
38073
37172
|
exports.FullTextPlugin = FullTextPlugin;
|
|
@@ -38113,6 +37212,7 @@ exports.S3dbReplicator = S3dbReplicator;
|
|
|
38113
37212
|
exports.SchedulerPlugin = SchedulerPlugin;
|
|
38114
37213
|
exports.Schema = Schema;
|
|
38115
37214
|
exports.SchemaError = SchemaError;
|
|
37215
|
+
exports.Seeder = Seeder;
|
|
38116
37216
|
exports.SqsConsumer = SqsConsumer;
|
|
38117
37217
|
exports.SqsReplicator = SqsReplicator;
|
|
38118
37218
|
exports.StateMachinePlugin = StateMachinePlugin;
|
|
@@ -38131,8 +37231,6 @@ exports.calculateEffectiveLimit = calculateEffectiveLimit;
|
|
|
38131
37231
|
exports.calculateSystemOverhead = calculateSystemOverhead;
|
|
38132
37232
|
exports.calculateTotalSize = calculateTotalSize;
|
|
38133
37233
|
exports.calculateUTF8Bytes = calculateUTF8Bytes;
|
|
38134
|
-
exports.clearUTF8Cache = clearUTF8Cache;
|
|
38135
|
-
exports.clearUTF8Memo = clearUTF8Memo;
|
|
38136
37234
|
exports.clearUTF8Memory = clearUTF8Memory;
|
|
38137
37235
|
exports.createBackupDriver = createBackupDriver;
|
|
38138
37236
|
exports.createConsumer = createConsumer;
|
|
@@ -38148,12 +37246,14 @@ exports.encodeDecimal = encodeDecimal;
|
|
|
38148
37246
|
exports.encodeFixedPoint = encodeFixedPoint;
|
|
38149
37247
|
exports.encodeFixedPointBatch = encodeFixedPointBatch;
|
|
38150
37248
|
exports.encrypt = encrypt;
|
|
37249
|
+
exports.generateTypes = generateTypes;
|
|
38151
37250
|
exports.getBehavior = getBehavior;
|
|
38152
37251
|
exports.getSizeBreakdown = getSizeBreakdown;
|
|
38153
37252
|
exports.idGenerator = idGenerator;
|
|
38154
37253
|
exports.mapAwsError = mapAwsError;
|
|
38155
37254
|
exports.md5 = md5;
|
|
38156
37255
|
exports.passwordGenerator = passwordGenerator;
|
|
37256
|
+
exports.printTypes = printTypes;
|
|
38157
37257
|
exports.sha256 = sha256;
|
|
38158
37258
|
exports.streamToString = streamToString;
|
|
38159
37259
|
exports.transformValue = transformValue;
|