s3db.js 13.0.0 → 13.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 +9 -9
- package/dist/s3db.cjs.js +3637 -191
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +3637 -191
- package/dist/s3db.es.js.map +1 -1
- package/package.json +2 -1
- package/src/clients/memory-client.class.js +16 -16
- package/src/clients/s3-client.class.js +17 -17
- package/src/concerns/error-classifier.js +204 -0
- package/src/database.class.js +9 -9
- package/src/plugins/api/index.js +1 -7
- package/src/plugins/api/routes/resource-routes.js +3 -3
- package/src/plugins/api/server.js +29 -9
- package/src/plugins/audit.plugin.js +2 -4
- package/src/plugins/backup.plugin.js +10 -12
- package/src/plugins/cache.plugin.js +4 -6
- package/src/plugins/concerns/plugin-dependencies.js +12 -0
- package/src/plugins/costs.plugin.js +0 -2
- package/src/plugins/eventual-consistency/index.js +1 -3
- package/src/plugins/fulltext.plugin.js +2 -4
- package/src/plugins/geo.plugin.js +3 -5
- package/src/plugins/importer/index.js +0 -2
- package/src/plugins/index.js +0 -1
- package/src/plugins/metrics.plugin.js +2 -4
- package/src/plugins/ml.plugin.js +1004 -42
- package/src/plugins/plugin.class.js +1 -3
- package/src/plugins/queue-consumer.plugin.js +1 -3
- package/src/plugins/relation.plugin.js +2 -4
- package/src/plugins/replicator.plugin.js +18 -20
- package/src/plugins/s3-queue.plugin.js +6 -8
- package/src/plugins/scheduler.plugin.js +9 -11
- package/src/plugins/state-machine.errors.js +9 -1
- package/src/plugins/state-machine.plugin.js +605 -20
- package/src/plugins/tfstate/index.js +0 -2
- package/src/plugins/ttl.plugin.js +40 -25
- package/src/plugins/vector.plugin.js +10 -12
- package/src/resource.class.js +58 -40
package/dist/s3db.es.js
CHANGED
|
@@ -2544,6 +2544,18 @@ const PLUGIN_DEPENDENCIES = {
|
|
|
2544
2544
|
npmUrl: "https://www.npmjs.com/package/@hono/swagger-ui"
|
|
2545
2545
|
}
|
|
2546
2546
|
}
|
|
2547
|
+
},
|
|
2548
|
+
"ml-plugin": {
|
|
2549
|
+
name: "ML Plugin",
|
|
2550
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/ml-plugin.md",
|
|
2551
|
+
dependencies: {
|
|
2552
|
+
"@tensorflow/tfjs-node": {
|
|
2553
|
+
version: "^4.0.0",
|
|
2554
|
+
description: "TensorFlow.js for Node.js with native bindings",
|
|
2555
|
+
installCommand: "pnpm add @tensorflow/tfjs-node",
|
|
2556
|
+
npmUrl: "https://www.npmjs.com/package/@tensorflow/tfjs-node"
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2547
2559
|
}
|
|
2548
2560
|
};
|
|
2549
2561
|
function isVersionCompatible(actual, required) {
|
|
@@ -2680,6 +2692,2037 @@ async function requirePluginDependency(pluginId, options = {}) {
|
|
|
2680
2692
|
return { valid, missing, incompatible, messages };
|
|
2681
2693
|
}
|
|
2682
2694
|
|
|
2695
|
+
function success(data, options = {}) {
|
|
2696
|
+
const { status = 200, meta = {} } = options;
|
|
2697
|
+
return {
|
|
2698
|
+
success: true,
|
|
2699
|
+
data,
|
|
2700
|
+
meta: {
|
|
2701
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2702
|
+
...meta
|
|
2703
|
+
},
|
|
2704
|
+
_status: status
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
function error(error2, options = {}) {
|
|
2708
|
+
const { status = 500, code = "INTERNAL_ERROR", details = {} } = options;
|
|
2709
|
+
const errorMessage = error2 instanceof Error ? error2.message : error2;
|
|
2710
|
+
const errorStack = error2 instanceof Error && process.env.NODE_ENV !== "production" ? error2.stack : void 0;
|
|
2711
|
+
return {
|
|
2712
|
+
success: false,
|
|
2713
|
+
error: {
|
|
2714
|
+
message: errorMessage,
|
|
2715
|
+
code,
|
|
2716
|
+
details,
|
|
2717
|
+
stack: errorStack
|
|
2718
|
+
},
|
|
2719
|
+
meta: {
|
|
2720
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2721
|
+
},
|
|
2722
|
+
_status: status
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
function list(items, pagination = {}) {
|
|
2726
|
+
const { total, page, pageSize, pageCount } = pagination;
|
|
2727
|
+
return {
|
|
2728
|
+
success: true,
|
|
2729
|
+
data: items,
|
|
2730
|
+
pagination: {
|
|
2731
|
+
total: total || items.length,
|
|
2732
|
+
page: page || 1,
|
|
2733
|
+
pageSize: pageSize || items.length,
|
|
2734
|
+
pageCount: pageCount || 1
|
|
2735
|
+
},
|
|
2736
|
+
meta: {
|
|
2737
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2738
|
+
},
|
|
2739
|
+
_status: 200
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
function created(data, location) {
|
|
2743
|
+
return {
|
|
2744
|
+
success: true,
|
|
2745
|
+
data,
|
|
2746
|
+
meta: {
|
|
2747
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2748
|
+
location
|
|
2749
|
+
},
|
|
2750
|
+
_status: 201
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
function noContent() {
|
|
2754
|
+
return {
|
|
2755
|
+
success: true,
|
|
2756
|
+
data: null,
|
|
2757
|
+
meta: {
|
|
2758
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2759
|
+
},
|
|
2760
|
+
_status: 204
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
function notFound(resource, id) {
|
|
2764
|
+
return error(`${resource} with id '${id}' not found`, {
|
|
2765
|
+
status: 404,
|
|
2766
|
+
code: "NOT_FOUND",
|
|
2767
|
+
details: { resource, id }
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
function payloadTooLarge(size, limit) {
|
|
2771
|
+
return error("Request payload too large", {
|
|
2772
|
+
status: 413,
|
|
2773
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
2774
|
+
details: {
|
|
2775
|
+
receivedSize: size,
|
|
2776
|
+
maxSize: limit,
|
|
2777
|
+
receivedMB: (size / 1024 / 1024).toFixed(2),
|
|
2778
|
+
maxMB: (limit / 1024 / 1024).toFixed(2)
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
const errorStatusMap = {
|
|
2784
|
+
"ValidationError": 400,
|
|
2785
|
+
"InvalidResourceItem": 400,
|
|
2786
|
+
"ResourceNotFound": 404,
|
|
2787
|
+
"NoSuchKey": 404,
|
|
2788
|
+
"NoSuchBucket": 404,
|
|
2789
|
+
"PartitionError": 400,
|
|
2790
|
+
"CryptoError": 500,
|
|
2791
|
+
"SchemaError": 400,
|
|
2792
|
+
"QueueError": 500,
|
|
2793
|
+
"ResourceError": 500
|
|
2794
|
+
};
|
|
2795
|
+
function getStatusFromError(err) {
|
|
2796
|
+
if (err.name && errorStatusMap[err.name]) {
|
|
2797
|
+
return errorStatusMap[err.name];
|
|
2798
|
+
}
|
|
2799
|
+
if (err.constructor && err.constructor.name && errorStatusMap[err.constructor.name]) {
|
|
2800
|
+
return errorStatusMap[err.constructor.name];
|
|
2801
|
+
}
|
|
2802
|
+
if (err.message) {
|
|
2803
|
+
if (err.message.includes("not found") || err.message.includes("does not exist")) {
|
|
2804
|
+
return 404;
|
|
2805
|
+
}
|
|
2806
|
+
if (err.message.includes("validation") || err.message.includes("invalid")) {
|
|
2807
|
+
return 400;
|
|
2808
|
+
}
|
|
2809
|
+
if (err.message.includes("unauthorized") || err.message.includes("authentication")) {
|
|
2810
|
+
return 401;
|
|
2811
|
+
}
|
|
2812
|
+
if (err.message.includes("forbidden") || err.message.includes("permission")) {
|
|
2813
|
+
return 403;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
return 500;
|
|
2817
|
+
}
|
|
2818
|
+
function errorHandler(err, c) {
|
|
2819
|
+
const status = getStatusFromError(err);
|
|
2820
|
+
const code = err.name || "INTERNAL_ERROR";
|
|
2821
|
+
const details = {};
|
|
2822
|
+
if (err.resource) details.resource = err.resource;
|
|
2823
|
+
if (err.bucket) details.bucket = err.bucket;
|
|
2824
|
+
if (err.key) details.key = err.key;
|
|
2825
|
+
if (err.operation) details.operation = err.operation;
|
|
2826
|
+
if (err.suggestion) details.suggestion = err.suggestion;
|
|
2827
|
+
if (err.availableResources) details.availableResources = err.availableResources;
|
|
2828
|
+
const response = error(err, {
|
|
2829
|
+
status,
|
|
2830
|
+
code,
|
|
2831
|
+
details
|
|
2832
|
+
});
|
|
2833
|
+
if (status >= 500) {
|
|
2834
|
+
console.error("[API Plugin] Error:", {
|
|
2835
|
+
message: err.message,
|
|
2836
|
+
code,
|
|
2837
|
+
status,
|
|
2838
|
+
stack: err.stack,
|
|
2839
|
+
details
|
|
2840
|
+
});
|
|
2841
|
+
} else if (status >= 400 && status < 500 && c.get("verbose")) {
|
|
2842
|
+
console.warn("[API Plugin] Client error:", {
|
|
2843
|
+
message: err.message,
|
|
2844
|
+
code,
|
|
2845
|
+
status,
|
|
2846
|
+
details
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
return c.json(response, response._status);
|
|
2850
|
+
}
|
|
2851
|
+
function asyncHandler(fn) {
|
|
2852
|
+
return async (c) => {
|
|
2853
|
+
try {
|
|
2854
|
+
return await fn(c);
|
|
2855
|
+
} catch (err) {
|
|
2856
|
+
return errorHandler(err, c);
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
function parseCustomRoute(routeDef) {
|
|
2862
|
+
let def = routeDef.trim();
|
|
2863
|
+
const isAsync = def.startsWith("async ");
|
|
2864
|
+
if (isAsync) {
|
|
2865
|
+
def = def.substring(6).trim();
|
|
2866
|
+
}
|
|
2867
|
+
const parts = def.split(/\s+/);
|
|
2868
|
+
if (parts.length < 2) {
|
|
2869
|
+
throw new Error(`Invalid route definition: "${routeDef}". Expected format: "METHOD /path" or "async METHOD /path"`);
|
|
2870
|
+
}
|
|
2871
|
+
const method = parts[0].toUpperCase();
|
|
2872
|
+
const path = parts.slice(1).join(" ").trim();
|
|
2873
|
+
const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
2874
|
+
if (!validMethods.includes(method)) {
|
|
2875
|
+
throw new Error(`Invalid HTTP method: "${method}". Must be one of: ${validMethods.join(", ")}`);
|
|
2876
|
+
}
|
|
2877
|
+
if (!path.startsWith("/")) {
|
|
2878
|
+
throw new Error(`Invalid route path: "${path}". Path must start with "/"`);
|
|
2879
|
+
}
|
|
2880
|
+
return { method, path, isAsync };
|
|
2881
|
+
}
|
|
2882
|
+
function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
2883
|
+
const app = new Hono();
|
|
2884
|
+
const {
|
|
2885
|
+
methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
2886
|
+
customMiddleware = [],
|
|
2887
|
+
enableValidation = true
|
|
2888
|
+
} = config;
|
|
2889
|
+
const resourceName = resource.name;
|
|
2890
|
+
const basePath = `/${version}/${resourceName}`;
|
|
2891
|
+
customMiddleware.forEach((middleware) => {
|
|
2892
|
+
app.use("*", middleware);
|
|
2893
|
+
});
|
|
2894
|
+
if (resource.config?.api && typeof resource.config.api === "object") {
|
|
2895
|
+
for (const [routeDef, handler] of Object.entries(resource.config.api)) {
|
|
2896
|
+
try {
|
|
2897
|
+
const { method, path } = parseCustomRoute(routeDef);
|
|
2898
|
+
if (typeof handler !== "function") {
|
|
2899
|
+
throw new Error(`Handler for route "${routeDef}" must be a function`);
|
|
2900
|
+
}
|
|
2901
|
+
app.on(method, path, asyncHandler(async (c) => {
|
|
2902
|
+
const result = await handler(c, { resource, database: resource.database });
|
|
2903
|
+
if (result && result.constructor && result.constructor.name === "Response") {
|
|
2904
|
+
return result;
|
|
2905
|
+
}
|
|
2906
|
+
if (result !== void 0 && result !== null) {
|
|
2907
|
+
return c.json(success(result));
|
|
2908
|
+
}
|
|
2909
|
+
return c.json(noContent(), 204);
|
|
2910
|
+
}));
|
|
2911
|
+
if (config.verbose || resource.database?.verbose) {
|
|
2912
|
+
console.log(`[API Plugin] Registered custom route for ${resourceName}: ${method} ${path}`);
|
|
2913
|
+
}
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
console.error(`[API Plugin] Error registering custom route "${routeDef}" for ${resourceName}:`, error.message);
|
|
2916
|
+
throw error;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
if (methods.includes("GET")) {
|
|
2921
|
+
app.get("/", asyncHandler(async (c) => {
|
|
2922
|
+
const query = c.req.query();
|
|
2923
|
+
const limit = parseInt(query.limit) || 100;
|
|
2924
|
+
const offset = parseInt(query.offset) || 0;
|
|
2925
|
+
const partition = query.partition;
|
|
2926
|
+
const partitionValues = query.partitionValues ? JSON.parse(query.partitionValues) : void 0;
|
|
2927
|
+
const reservedKeys = ["limit", "offset", "partition", "partitionValues", "sort"];
|
|
2928
|
+
const filters = {};
|
|
2929
|
+
for (const [key, value] of Object.entries(query)) {
|
|
2930
|
+
if (!reservedKeys.includes(key)) {
|
|
2931
|
+
try {
|
|
2932
|
+
filters[key] = JSON.parse(value);
|
|
2933
|
+
} catch {
|
|
2934
|
+
filters[key] = value;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
let items;
|
|
2939
|
+
let total;
|
|
2940
|
+
if (Object.keys(filters).length > 0) {
|
|
2941
|
+
items = await resource.query(filters, { limit: limit + offset });
|
|
2942
|
+
items = items.slice(offset, offset + limit);
|
|
2943
|
+
total = items.length;
|
|
2944
|
+
} else if (partition && partitionValues) {
|
|
2945
|
+
items = await resource.listPartition({
|
|
2946
|
+
partition,
|
|
2947
|
+
partitionValues,
|
|
2948
|
+
limit: limit + offset
|
|
2949
|
+
});
|
|
2950
|
+
items = items.slice(offset, offset + limit);
|
|
2951
|
+
total = items.length;
|
|
2952
|
+
} else {
|
|
2953
|
+
items = await resource.list({ limit: limit + offset });
|
|
2954
|
+
items = items.slice(offset, offset + limit);
|
|
2955
|
+
total = items.length;
|
|
2956
|
+
}
|
|
2957
|
+
const response = list(items, {
|
|
2958
|
+
total,
|
|
2959
|
+
page: Math.floor(offset / limit) + 1,
|
|
2960
|
+
pageSize: limit,
|
|
2961
|
+
pageCount: Math.ceil(total / limit)
|
|
2962
|
+
});
|
|
2963
|
+
c.header("X-Total-Count", total.toString());
|
|
2964
|
+
c.header("X-Page-Count", Math.ceil(total / limit).toString());
|
|
2965
|
+
return c.json(response, response._status);
|
|
2966
|
+
}));
|
|
2967
|
+
}
|
|
2968
|
+
if (methods.includes("GET")) {
|
|
2969
|
+
app.get("/:id", asyncHandler(async (c) => {
|
|
2970
|
+
const id = c.req.param("id");
|
|
2971
|
+
const query = c.req.query();
|
|
2972
|
+
const partition = query.partition;
|
|
2973
|
+
const partitionValues = query.partitionValues ? JSON.parse(query.partitionValues) : void 0;
|
|
2974
|
+
let item;
|
|
2975
|
+
if (partition && partitionValues) {
|
|
2976
|
+
item = await resource.getFromPartition({
|
|
2977
|
+
id,
|
|
2978
|
+
partitionName: partition,
|
|
2979
|
+
partitionValues
|
|
2980
|
+
});
|
|
2981
|
+
} else {
|
|
2982
|
+
item = await resource.get(id);
|
|
2983
|
+
}
|
|
2984
|
+
if (!item) {
|
|
2985
|
+
const response2 = notFound(resourceName, id);
|
|
2986
|
+
return c.json(response2, response2._status);
|
|
2987
|
+
}
|
|
2988
|
+
const response = success(item);
|
|
2989
|
+
return c.json(response, response._status);
|
|
2990
|
+
}));
|
|
2991
|
+
}
|
|
2992
|
+
if (methods.includes("POST")) {
|
|
2993
|
+
app.post("/", asyncHandler(async (c) => {
|
|
2994
|
+
const data = await c.req.json();
|
|
2995
|
+
const item = await resource.insert(data);
|
|
2996
|
+
const location = `${basePath}/${item.id}`;
|
|
2997
|
+
const response = created(item, location);
|
|
2998
|
+
c.header("Location", location);
|
|
2999
|
+
return c.json(response, response._status);
|
|
3000
|
+
}));
|
|
3001
|
+
}
|
|
3002
|
+
if (methods.includes("PUT")) {
|
|
3003
|
+
app.put("/:id", asyncHandler(async (c) => {
|
|
3004
|
+
const id = c.req.param("id");
|
|
3005
|
+
const data = await c.req.json();
|
|
3006
|
+
const existing = await resource.get(id);
|
|
3007
|
+
if (!existing) {
|
|
3008
|
+
const response2 = notFound(resourceName, id);
|
|
3009
|
+
return c.json(response2, response2._status);
|
|
3010
|
+
}
|
|
3011
|
+
const updated = await resource.update(id, data);
|
|
3012
|
+
const response = success(updated);
|
|
3013
|
+
return c.json(response, response._status);
|
|
3014
|
+
}));
|
|
3015
|
+
}
|
|
3016
|
+
if (methods.includes("PATCH")) {
|
|
3017
|
+
app.patch("/:id", asyncHandler(async (c) => {
|
|
3018
|
+
const id = c.req.param("id");
|
|
3019
|
+
const data = await c.req.json();
|
|
3020
|
+
const existing = await resource.get(id);
|
|
3021
|
+
if (!existing) {
|
|
3022
|
+
const response2 = notFound(resourceName, id);
|
|
3023
|
+
return c.json(response2, response2._status);
|
|
3024
|
+
}
|
|
3025
|
+
const merged = { ...existing, ...data, id };
|
|
3026
|
+
const updated = await resource.update(id, merged);
|
|
3027
|
+
const response = success(updated);
|
|
3028
|
+
return c.json(response, response._status);
|
|
3029
|
+
}));
|
|
3030
|
+
}
|
|
3031
|
+
if (methods.includes("DELETE")) {
|
|
3032
|
+
app.delete("/:id", asyncHandler(async (c) => {
|
|
3033
|
+
const id = c.req.param("id");
|
|
3034
|
+
const existing = await resource.get(id);
|
|
3035
|
+
if (!existing) {
|
|
3036
|
+
const response2 = notFound(resourceName, id);
|
|
3037
|
+
return c.json(response2, response2._status);
|
|
3038
|
+
}
|
|
3039
|
+
await resource.delete(id);
|
|
3040
|
+
const response = noContent();
|
|
3041
|
+
return c.json(response, response._status);
|
|
3042
|
+
}));
|
|
3043
|
+
}
|
|
3044
|
+
if (methods.includes("HEAD")) {
|
|
3045
|
+
app.on("HEAD", "/", asyncHandler(async (c) => {
|
|
3046
|
+
const total = await resource.count();
|
|
3047
|
+
const allItems = await resource.list({ limit: 1e3 });
|
|
3048
|
+
const stats = {
|
|
3049
|
+
total,
|
|
3050
|
+
version: resource.config?.currentVersion || resource.version || "v1"
|
|
3051
|
+
};
|
|
3052
|
+
c.header("X-Total-Count", total.toString());
|
|
3053
|
+
c.header("X-Resource-Version", stats.version);
|
|
3054
|
+
c.header("X-Schema-Fields", Object.keys(resource.config?.attributes || {}).length.toString());
|
|
3055
|
+
return c.body(null, 200);
|
|
3056
|
+
}));
|
|
3057
|
+
app.on("HEAD", "/:id", asyncHandler(async (c) => {
|
|
3058
|
+
const id = c.req.param("id");
|
|
3059
|
+
const item = await resource.get(id);
|
|
3060
|
+
if (!item) {
|
|
3061
|
+
return c.body(null, 404);
|
|
3062
|
+
}
|
|
3063
|
+
if (item.updatedAt) {
|
|
3064
|
+
c.header("Last-Modified", new Date(item.updatedAt).toUTCString());
|
|
3065
|
+
}
|
|
3066
|
+
return c.body(null, 200);
|
|
3067
|
+
}));
|
|
3068
|
+
}
|
|
3069
|
+
if (methods.includes("OPTIONS")) {
|
|
3070
|
+
app.options("/", asyncHandler(async (c) => {
|
|
3071
|
+
c.header("Allow", methods.join(", "));
|
|
3072
|
+
const total = await resource.count();
|
|
3073
|
+
const schema = resource.config?.attributes || {};
|
|
3074
|
+
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
3075
|
+
const metadata = {
|
|
3076
|
+
resource: resourceName,
|
|
3077
|
+
version: version2,
|
|
3078
|
+
totalRecords: total,
|
|
3079
|
+
allowedMethods: methods,
|
|
3080
|
+
schema: Object.entries(schema).map(([name, def]) => ({
|
|
3081
|
+
name,
|
|
3082
|
+
type: typeof def === "string" ? def.split("|")[0] : def.type,
|
|
3083
|
+
rules: typeof def === "string" ? def.split("|").slice(1) : []
|
|
3084
|
+
})),
|
|
3085
|
+
endpoints: {
|
|
3086
|
+
list: `/${version2}/${resourceName}`,
|
|
3087
|
+
get: `/${version2}/${resourceName}/:id`,
|
|
3088
|
+
create: `/${version2}/${resourceName}`,
|
|
3089
|
+
update: `/${version2}/${resourceName}/:id`,
|
|
3090
|
+
delete: `/${version2}/${resourceName}/:id`
|
|
3091
|
+
},
|
|
3092
|
+
queryParameters: {
|
|
3093
|
+
limit: "number (1-1000, default: 100)",
|
|
3094
|
+
offset: "number (min: 0, default: 0)",
|
|
3095
|
+
partition: "string (partition name)",
|
|
3096
|
+
partitionValues: "JSON string",
|
|
3097
|
+
"[any field]": "any (filter by field value)"
|
|
3098
|
+
}
|
|
3099
|
+
};
|
|
3100
|
+
return c.json(metadata);
|
|
3101
|
+
}));
|
|
3102
|
+
app.options("/:id", (c) => {
|
|
3103
|
+
c.header("Allow", methods.filter((m) => m !== "POST").join(", "));
|
|
3104
|
+
return c.body(null, 204);
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
return app;
|
|
3108
|
+
}
|
|
3109
|
+
function createRelationalRoutes(sourceResource, relationName, relationConfig, version, Hono) {
|
|
3110
|
+
const app = new Hono();
|
|
3111
|
+
const resourceName = sourceResource.name;
|
|
3112
|
+
const relatedResourceName = relationConfig.resource;
|
|
3113
|
+
app.get("/:id", asyncHandler(async (c) => {
|
|
3114
|
+
const id = c.req.param("id");
|
|
3115
|
+
const query = c.req.query();
|
|
3116
|
+
const source = await sourceResource.get(id);
|
|
3117
|
+
if (!source) {
|
|
3118
|
+
const response = notFound(resourceName, id);
|
|
3119
|
+
return c.json(response, response._status);
|
|
3120
|
+
}
|
|
3121
|
+
const result = await sourceResource.get(id, {
|
|
3122
|
+
include: [relationName]
|
|
3123
|
+
});
|
|
3124
|
+
const relatedData = result[relationName];
|
|
3125
|
+
if (!relatedData) {
|
|
3126
|
+
if (relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany") {
|
|
3127
|
+
const response = list([], {
|
|
3128
|
+
total: 0,
|
|
3129
|
+
page: 1,
|
|
3130
|
+
pageSize: 100,
|
|
3131
|
+
pageCount: 0
|
|
3132
|
+
});
|
|
3133
|
+
return c.json(response, response._status);
|
|
3134
|
+
} else {
|
|
3135
|
+
const response = notFound(relatedResourceName, "related resource");
|
|
3136
|
+
return c.json(response, response._status);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany") {
|
|
3140
|
+
const items = Array.isArray(relatedData) ? relatedData : [relatedData];
|
|
3141
|
+
const limit = parseInt(query.limit) || 100;
|
|
3142
|
+
const offset = parseInt(query.offset) || 0;
|
|
3143
|
+
const paginatedItems = items.slice(offset, offset + limit);
|
|
3144
|
+
const response = list(paginatedItems, {
|
|
3145
|
+
total: items.length,
|
|
3146
|
+
page: Math.floor(offset / limit) + 1,
|
|
3147
|
+
pageSize: limit,
|
|
3148
|
+
pageCount: Math.ceil(items.length / limit)
|
|
3149
|
+
});
|
|
3150
|
+
c.header("X-Total-Count", items.length.toString());
|
|
3151
|
+
c.header("X-Page-Count", Math.ceil(items.length / limit).toString());
|
|
3152
|
+
return c.json(response, response._status);
|
|
3153
|
+
} else {
|
|
3154
|
+
const response = success(relatedData);
|
|
3155
|
+
return c.json(response, response._status);
|
|
3156
|
+
}
|
|
3157
|
+
}));
|
|
3158
|
+
return app;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
function mapFieldTypeToOpenAPI(fieldType) {
|
|
3162
|
+
const type = fieldType.split("|")[0].trim();
|
|
3163
|
+
const typeMap = {
|
|
3164
|
+
"string": { type: "string" },
|
|
3165
|
+
"number": { type: "number" },
|
|
3166
|
+
"integer": { type: "integer" },
|
|
3167
|
+
"boolean": { type: "boolean" },
|
|
3168
|
+
"array": { type: "array", items: { type: "string" } },
|
|
3169
|
+
"object": { type: "object" },
|
|
3170
|
+
"json": { type: "object" },
|
|
3171
|
+
"secret": { type: "string", format: "password" },
|
|
3172
|
+
"email": { type: "string", format: "email" },
|
|
3173
|
+
"url": { type: "string", format: "uri" },
|
|
3174
|
+
"date": { type: "string", format: "date" },
|
|
3175
|
+
"datetime": { type: "string", format: "date-time" },
|
|
3176
|
+
"ip4": { type: "string", format: "ipv4", description: "IPv4 address" },
|
|
3177
|
+
"ip6": { type: "string", format: "ipv6", description: "IPv6 address" },
|
|
3178
|
+
"embedding": { type: "array", items: { type: "number" }, description: "Vector embedding" }
|
|
3179
|
+
};
|
|
3180
|
+
if (type.startsWith("embedding:")) {
|
|
3181
|
+
const length = parseInt(type.split(":")[1]);
|
|
3182
|
+
return {
|
|
3183
|
+
type: "array",
|
|
3184
|
+
items: { type: "number" },
|
|
3185
|
+
minItems: length,
|
|
3186
|
+
maxItems: length,
|
|
3187
|
+
description: `Vector embedding (${length} dimensions)`
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
return typeMap[type] || { type: "string" };
|
|
3191
|
+
}
|
|
3192
|
+
function extractValidationRules(fieldDef) {
|
|
3193
|
+
const rules = {};
|
|
3194
|
+
const parts = fieldDef.split("|");
|
|
3195
|
+
for (const part of parts) {
|
|
3196
|
+
const [rule, value] = part.split(":").map((s) => s.trim());
|
|
3197
|
+
switch (rule) {
|
|
3198
|
+
case "required":
|
|
3199
|
+
rules.required = true;
|
|
3200
|
+
break;
|
|
3201
|
+
case "min":
|
|
3202
|
+
rules.minimum = parseFloat(value);
|
|
3203
|
+
break;
|
|
3204
|
+
case "max":
|
|
3205
|
+
rules.maximum = parseFloat(value);
|
|
3206
|
+
break;
|
|
3207
|
+
case "minlength":
|
|
3208
|
+
rules.minLength = parseInt(value);
|
|
3209
|
+
break;
|
|
3210
|
+
case "maxlength":
|
|
3211
|
+
rules.maxLength = parseInt(value);
|
|
3212
|
+
break;
|
|
3213
|
+
case "pattern":
|
|
3214
|
+
rules.pattern = value;
|
|
3215
|
+
break;
|
|
3216
|
+
case "enum":
|
|
3217
|
+
rules.enum = value.split(",").map((v) => v.trim());
|
|
3218
|
+
break;
|
|
3219
|
+
case "default":
|
|
3220
|
+
rules.default = value;
|
|
3221
|
+
break;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return rules;
|
|
3225
|
+
}
|
|
3226
|
+
function generateResourceSchema(resource) {
|
|
3227
|
+
const properties = {};
|
|
3228
|
+
const required = [];
|
|
3229
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
3230
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
3231
|
+
const attributes = Object.fromEntries(
|
|
3232
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
3233
|
+
);
|
|
3234
|
+
const resourceDescription = resource.config?.description;
|
|
3235
|
+
const attributeDescriptions = typeof resourceDescription === "object" ? resourceDescription.attributes || {} : {};
|
|
3236
|
+
properties.id = {
|
|
3237
|
+
type: "string",
|
|
3238
|
+
description: "Unique identifier for the resource",
|
|
3239
|
+
example: "2_gDTpeU6EI0e8B92n_R3Y",
|
|
3240
|
+
readOnly: true
|
|
3241
|
+
};
|
|
3242
|
+
for (const [fieldName, fieldDef] of Object.entries(attributes)) {
|
|
3243
|
+
if (typeof fieldDef === "object" && fieldDef.type) {
|
|
3244
|
+
const baseType = mapFieldTypeToOpenAPI(fieldDef.type);
|
|
3245
|
+
properties[fieldName] = {
|
|
3246
|
+
...baseType,
|
|
3247
|
+
description: fieldDef.description || attributeDescriptions[fieldName] || void 0
|
|
3248
|
+
};
|
|
3249
|
+
if (fieldDef.required) {
|
|
3250
|
+
required.push(fieldName);
|
|
3251
|
+
}
|
|
3252
|
+
if (fieldDef.type === "object" && fieldDef.props) {
|
|
3253
|
+
properties[fieldName].properties = {};
|
|
3254
|
+
for (const [propName, propDef] of Object.entries(fieldDef.props)) {
|
|
3255
|
+
const propType = typeof propDef === "string" ? propDef : propDef.type;
|
|
3256
|
+
properties[fieldName].properties[propName] = mapFieldTypeToOpenAPI(propType);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
if (fieldDef.type === "array" && fieldDef.items) {
|
|
3260
|
+
properties[fieldName].items = mapFieldTypeToOpenAPI(fieldDef.items);
|
|
3261
|
+
}
|
|
3262
|
+
} else if (typeof fieldDef === "string") {
|
|
3263
|
+
const baseType = mapFieldTypeToOpenAPI(fieldDef);
|
|
3264
|
+
const rules = extractValidationRules(fieldDef);
|
|
3265
|
+
properties[fieldName] = {
|
|
3266
|
+
...baseType,
|
|
3267
|
+
...rules,
|
|
3268
|
+
description: attributeDescriptions[fieldName] || void 0
|
|
3269
|
+
};
|
|
3270
|
+
if (rules.required) {
|
|
3271
|
+
required.push(fieldName);
|
|
3272
|
+
delete properties[fieldName].required;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return {
|
|
3277
|
+
type: "object",
|
|
3278
|
+
properties,
|
|
3279
|
+
required: required.length > 0 ? required : void 0
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
function generateResourcePaths(resource, version, config = {}) {
|
|
3283
|
+
const resourceName = resource.name;
|
|
3284
|
+
const basePath = `/${version}/${resourceName}`;
|
|
3285
|
+
const schema = generateResourceSchema(resource);
|
|
3286
|
+
const methods = config.methods || ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
3287
|
+
const authMethods = config.auth || [];
|
|
3288
|
+
const requiresAuth = authMethods && authMethods.length > 0;
|
|
3289
|
+
const paths = {};
|
|
3290
|
+
const security = [];
|
|
3291
|
+
if (requiresAuth) {
|
|
3292
|
+
if (authMethods.includes("jwt")) security.push({ bearerAuth: [] });
|
|
3293
|
+
if (authMethods.includes("apiKey")) security.push({ apiKeyAuth: [] });
|
|
3294
|
+
if (authMethods.includes("basic")) security.push({ basicAuth: [] });
|
|
3295
|
+
}
|
|
3296
|
+
const partitions = resource.config?.options?.partitions || resource.config?.partitions || resource.partitions || {};
|
|
3297
|
+
const partitionNames = Object.keys(partitions);
|
|
3298
|
+
const hasPartitions = partitionNames.length > 0;
|
|
3299
|
+
let partitionDescription = "Partition name for filtering";
|
|
3300
|
+
let partitionValuesDescription = "Partition values as JSON string";
|
|
3301
|
+
let partitionExample = void 0;
|
|
3302
|
+
let partitionValuesExample = void 0;
|
|
3303
|
+
if (hasPartitions) {
|
|
3304
|
+
const partitionDocs = partitionNames.map((name) => {
|
|
3305
|
+
const partition = partitions[name];
|
|
3306
|
+
const fields = Object.keys(partition.fields || {});
|
|
3307
|
+
const fieldTypes = Object.entries(partition.fields || {}).map(([field, type]) => `${field}: ${type}`).join(", ");
|
|
3308
|
+
return `- **${name}**: Filters by ${fields.join(", ")} (${fieldTypes})`;
|
|
3309
|
+
}).join("\n");
|
|
3310
|
+
partitionDescription = `Available partitions:
|
|
3311
|
+
${partitionDocs}`;
|
|
3312
|
+
const examplePartition = partitionNames[0];
|
|
3313
|
+
const exampleFields = partitions[examplePartition]?.fields || {};
|
|
3314
|
+
Object.entries(exampleFields).map(([field, type]) => `"${field}": <${type} value>`).join(", ");
|
|
3315
|
+
partitionValuesDescription = `Partition field values as JSON string. Must match the structure of the selected partition.
|
|
3316
|
+
|
|
3317
|
+
Example for "${examplePartition}" partition: \`{"${Object.keys(exampleFields)[0]}": "value"}\``;
|
|
3318
|
+
partitionExample = examplePartition;
|
|
3319
|
+
const firstField = Object.keys(exampleFields)[0];
|
|
3320
|
+
const firstFieldType = exampleFields[firstField];
|
|
3321
|
+
let exampleValue = "example";
|
|
3322
|
+
if (firstFieldType === "number" || firstFieldType === "integer") {
|
|
3323
|
+
exampleValue = 123;
|
|
3324
|
+
} else if (firstFieldType === "boolean") {
|
|
3325
|
+
exampleValue = true;
|
|
3326
|
+
}
|
|
3327
|
+
partitionValuesExample = JSON.stringify({ [firstField]: exampleValue });
|
|
3328
|
+
}
|
|
3329
|
+
const attributeQueryParams = [];
|
|
3330
|
+
if (hasPartitions) {
|
|
3331
|
+
const partitionFieldsSet = /* @__PURE__ */ new Set();
|
|
3332
|
+
for (const [partitionName, partition] of Object.entries(partitions)) {
|
|
3333
|
+
const fields = partition.fields || {};
|
|
3334
|
+
for (const fieldName of Object.keys(fields)) {
|
|
3335
|
+
partitionFieldsSet.add(fieldName);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
3339
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
3340
|
+
const attributes = Object.fromEntries(
|
|
3341
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
3342
|
+
);
|
|
3343
|
+
for (const fieldName of partitionFieldsSet) {
|
|
3344
|
+
const fieldDef = attributes[fieldName];
|
|
3345
|
+
if (!fieldDef) continue;
|
|
3346
|
+
let fieldType;
|
|
3347
|
+
if (typeof fieldDef === "object" && fieldDef.type) {
|
|
3348
|
+
fieldType = fieldDef.type;
|
|
3349
|
+
} else if (typeof fieldDef === "string") {
|
|
3350
|
+
fieldType = fieldDef.split("|")[0].trim();
|
|
3351
|
+
} else {
|
|
3352
|
+
fieldType = "string";
|
|
3353
|
+
}
|
|
3354
|
+
const openAPIType = mapFieldTypeToOpenAPI(fieldType);
|
|
3355
|
+
attributeQueryParams.push({
|
|
3356
|
+
name: fieldName,
|
|
3357
|
+
in: "query",
|
|
3358
|
+
description: `Filter by ${fieldName} field (indexed via partitions for efficient querying). Value will be parsed as JSON if possible, otherwise treated as string.`,
|
|
3359
|
+
required: false,
|
|
3360
|
+
schema: openAPIType
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
if (methods.includes("GET")) {
|
|
3365
|
+
paths[basePath] = {
|
|
3366
|
+
get: {
|
|
3367
|
+
tags: [resourceName],
|
|
3368
|
+
summary: `List ${resourceName}`,
|
|
3369
|
+
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.
|
|
3370
|
+
|
|
3371
|
+
**Pagination**: Use \`limit\` and \`offset\` to paginate results. For example:
|
|
3372
|
+
- First page (10 items): \`?limit=10&offset=0\`
|
|
3373
|
+
- Second page: \`?limit=10&offset=10\`
|
|
3374
|
+
- Third page: \`?limit=10&offset=20\`
|
|
3375
|
+
|
|
3376
|
+
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." : ""}`,
|
|
3377
|
+
parameters: [
|
|
3378
|
+
{
|
|
3379
|
+
name: "limit",
|
|
3380
|
+
in: "query",
|
|
3381
|
+
description: "Maximum number of items to return per page (page size)",
|
|
3382
|
+
schema: { type: "integer", default: 100, minimum: 1, maximum: 1e3 },
|
|
3383
|
+
example: 10
|
|
3384
|
+
},
|
|
3385
|
+
{
|
|
3386
|
+
name: "offset",
|
|
3387
|
+
in: "query",
|
|
3388
|
+
description: "Number of items to skip before starting to return results. Use for pagination: offset = (page - 1) * limit",
|
|
3389
|
+
schema: { type: "integer", default: 0, minimum: 0 },
|
|
3390
|
+
example: 0
|
|
3391
|
+
},
|
|
3392
|
+
...hasPartitions ? [
|
|
3393
|
+
{
|
|
3394
|
+
name: "partition",
|
|
3395
|
+
in: "query",
|
|
3396
|
+
description: partitionDescription,
|
|
3397
|
+
schema: {
|
|
3398
|
+
type: "string",
|
|
3399
|
+
enum: partitionNames
|
|
3400
|
+
},
|
|
3401
|
+
example: partitionExample
|
|
3402
|
+
},
|
|
3403
|
+
{
|
|
3404
|
+
name: "partitionValues",
|
|
3405
|
+
in: "query",
|
|
3406
|
+
description: partitionValuesDescription,
|
|
3407
|
+
schema: { type: "string" },
|
|
3408
|
+
example: partitionValuesExample
|
|
3409
|
+
}
|
|
3410
|
+
] : [],
|
|
3411
|
+
...attributeQueryParams
|
|
3412
|
+
],
|
|
3413
|
+
responses: {
|
|
3414
|
+
200: {
|
|
3415
|
+
description: "Successful response",
|
|
3416
|
+
content: {
|
|
3417
|
+
"application/json": {
|
|
3418
|
+
schema: {
|
|
3419
|
+
type: "object",
|
|
3420
|
+
properties: {
|
|
3421
|
+
success: { type: "boolean", example: true },
|
|
3422
|
+
data: {
|
|
3423
|
+
type: "array",
|
|
3424
|
+
items: schema
|
|
3425
|
+
},
|
|
3426
|
+
pagination: {
|
|
3427
|
+
type: "object",
|
|
3428
|
+
description: "Pagination metadata for the current request",
|
|
3429
|
+
properties: {
|
|
3430
|
+
total: {
|
|
3431
|
+
type: "integer",
|
|
3432
|
+
description: "Total number of items available",
|
|
3433
|
+
example: 150
|
|
3434
|
+
},
|
|
3435
|
+
page: {
|
|
3436
|
+
type: "integer",
|
|
3437
|
+
description: "Current page number (1-indexed)",
|
|
3438
|
+
example: 1
|
|
3439
|
+
},
|
|
3440
|
+
pageSize: {
|
|
3441
|
+
type: "integer",
|
|
3442
|
+
description: "Number of items per page (same as limit parameter)",
|
|
3443
|
+
example: 10
|
|
3444
|
+
},
|
|
3445
|
+
pageCount: {
|
|
3446
|
+
type: "integer",
|
|
3447
|
+
description: "Total number of pages available",
|
|
3448
|
+
example: 15
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
},
|
|
3456
|
+
headers: {
|
|
3457
|
+
"X-Total-Count": {
|
|
3458
|
+
description: "Total number of records",
|
|
3459
|
+
schema: { type: "integer" }
|
|
3460
|
+
},
|
|
3461
|
+
"X-Page-Count": {
|
|
3462
|
+
description: "Total number of pages",
|
|
3463
|
+
schema: { type: "integer" }
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
},
|
|
3468
|
+
security: security.length > 0 ? security : void 0
|
|
3469
|
+
}
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
if (methods.includes("GET")) {
|
|
3473
|
+
paths[`${basePath}/{id}`] = {
|
|
3474
|
+
get: {
|
|
3475
|
+
tags: [resourceName],
|
|
3476
|
+
summary: `Get ${resourceName} by ID`,
|
|
3477
|
+
description: `Retrieve a single ${resourceName} by its ID${hasPartitions ? ". Optionally specify a partition for more efficient retrieval." : ""}`,
|
|
3478
|
+
parameters: [
|
|
3479
|
+
{
|
|
3480
|
+
name: "id",
|
|
3481
|
+
in: "path",
|
|
3482
|
+
required: true,
|
|
3483
|
+
description: `${resourceName} ID`,
|
|
3484
|
+
schema: { type: "string" }
|
|
3485
|
+
},
|
|
3486
|
+
...hasPartitions ? [
|
|
3487
|
+
{
|
|
3488
|
+
name: "partition",
|
|
3489
|
+
in: "query",
|
|
3490
|
+
description: partitionDescription,
|
|
3491
|
+
schema: {
|
|
3492
|
+
type: "string",
|
|
3493
|
+
enum: partitionNames
|
|
3494
|
+
},
|
|
3495
|
+
example: partitionExample
|
|
3496
|
+
},
|
|
3497
|
+
{
|
|
3498
|
+
name: "partitionValues",
|
|
3499
|
+
in: "query",
|
|
3500
|
+
description: partitionValuesDescription,
|
|
3501
|
+
schema: { type: "string" },
|
|
3502
|
+
example: partitionValuesExample
|
|
3503
|
+
}
|
|
3504
|
+
] : []
|
|
3505
|
+
],
|
|
3506
|
+
responses: {
|
|
3507
|
+
200: {
|
|
3508
|
+
description: "Successful response",
|
|
3509
|
+
content: {
|
|
3510
|
+
"application/json": {
|
|
3511
|
+
schema: {
|
|
3512
|
+
type: "object",
|
|
3513
|
+
properties: {
|
|
3514
|
+
success: { type: "boolean", example: true },
|
|
3515
|
+
data: schema
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
},
|
|
3521
|
+
404: {
|
|
3522
|
+
description: "Resource not found",
|
|
3523
|
+
content: {
|
|
3524
|
+
"application/json": {
|
|
3525
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
},
|
|
3530
|
+
security: security.length > 0 ? security : void 0
|
|
3531
|
+
}
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
if (methods.includes("POST")) {
|
|
3535
|
+
if (!paths[basePath]) paths[basePath] = {};
|
|
3536
|
+
paths[basePath].post = {
|
|
3537
|
+
tags: [resourceName],
|
|
3538
|
+
summary: `Create ${resourceName}`,
|
|
3539
|
+
description: `Create a new ${resourceName}`,
|
|
3540
|
+
requestBody: {
|
|
3541
|
+
required: true,
|
|
3542
|
+
content: {
|
|
3543
|
+
"application/json": {
|
|
3544
|
+
schema
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
},
|
|
3548
|
+
responses: {
|
|
3549
|
+
201: {
|
|
3550
|
+
description: "Resource created successfully",
|
|
3551
|
+
content: {
|
|
3552
|
+
"application/json": {
|
|
3553
|
+
schema: {
|
|
3554
|
+
type: "object",
|
|
3555
|
+
properties: {
|
|
3556
|
+
success: { type: "boolean", example: true },
|
|
3557
|
+
data: schema
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
},
|
|
3562
|
+
headers: {
|
|
3563
|
+
Location: {
|
|
3564
|
+
description: "URL of the created resource",
|
|
3565
|
+
schema: { type: "string" }
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
},
|
|
3569
|
+
400: {
|
|
3570
|
+
description: "Validation error",
|
|
3571
|
+
content: {
|
|
3572
|
+
"application/json": {
|
|
3573
|
+
schema: { $ref: "#/components/schemas/ValidationError" }
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
},
|
|
3578
|
+
security: security.length > 0 ? security : void 0
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
if (methods.includes("PUT")) {
|
|
3582
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3583
|
+
paths[`${basePath}/{id}`].put = {
|
|
3584
|
+
tags: [resourceName],
|
|
3585
|
+
summary: `Update ${resourceName} (full)`,
|
|
3586
|
+
description: `Fully update a ${resourceName}`,
|
|
3587
|
+
parameters: [
|
|
3588
|
+
{
|
|
3589
|
+
name: "id",
|
|
3590
|
+
in: "path",
|
|
3591
|
+
required: true,
|
|
3592
|
+
schema: { type: "string" }
|
|
3593
|
+
}
|
|
3594
|
+
],
|
|
3595
|
+
requestBody: {
|
|
3596
|
+
required: true,
|
|
3597
|
+
content: {
|
|
3598
|
+
"application/json": {
|
|
3599
|
+
schema
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
},
|
|
3603
|
+
responses: {
|
|
3604
|
+
200: {
|
|
3605
|
+
description: "Resource updated successfully",
|
|
3606
|
+
content: {
|
|
3607
|
+
"application/json": {
|
|
3608
|
+
schema: {
|
|
3609
|
+
type: "object",
|
|
3610
|
+
properties: {
|
|
3611
|
+
success: { type: "boolean", example: true },
|
|
3612
|
+
data: schema
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
},
|
|
3618
|
+
404: {
|
|
3619
|
+
description: "Resource not found",
|
|
3620
|
+
content: {
|
|
3621
|
+
"application/json": {
|
|
3622
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
},
|
|
3627
|
+
security: security.length > 0 ? security : void 0
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
if (methods.includes("PATCH")) {
|
|
3631
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3632
|
+
paths[`${basePath}/{id}`].patch = {
|
|
3633
|
+
tags: [resourceName],
|
|
3634
|
+
summary: `Update ${resourceName} (partial)`,
|
|
3635
|
+
description: `Partially update a ${resourceName}`,
|
|
3636
|
+
parameters: [
|
|
3637
|
+
{
|
|
3638
|
+
name: "id",
|
|
3639
|
+
in: "path",
|
|
3640
|
+
required: true,
|
|
3641
|
+
schema: { type: "string" }
|
|
3642
|
+
}
|
|
3643
|
+
],
|
|
3644
|
+
requestBody: {
|
|
3645
|
+
required: true,
|
|
3646
|
+
content: {
|
|
3647
|
+
"application/json": {
|
|
3648
|
+
schema: {
|
|
3649
|
+
...schema,
|
|
3650
|
+
required: void 0
|
|
3651
|
+
// Partial updates don't require all fields
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
},
|
|
3656
|
+
responses: {
|
|
3657
|
+
200: {
|
|
3658
|
+
description: "Resource updated successfully",
|
|
3659
|
+
content: {
|
|
3660
|
+
"application/json": {
|
|
3661
|
+
schema: {
|
|
3662
|
+
type: "object",
|
|
3663
|
+
properties: {
|
|
3664
|
+
success: { type: "boolean", example: true },
|
|
3665
|
+
data: schema
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
},
|
|
3671
|
+
404: {
|
|
3672
|
+
description: "Resource not found",
|
|
3673
|
+
content: {
|
|
3674
|
+
"application/json": {
|
|
3675
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
},
|
|
3680
|
+
security: security.length > 0 ? security : void 0
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
if (methods.includes("DELETE")) {
|
|
3684
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3685
|
+
paths[`${basePath}/{id}`].delete = {
|
|
3686
|
+
tags: [resourceName],
|
|
3687
|
+
summary: `Delete ${resourceName}`,
|
|
3688
|
+
description: `Delete a ${resourceName} by ID`,
|
|
3689
|
+
parameters: [
|
|
3690
|
+
{
|
|
3691
|
+
name: "id",
|
|
3692
|
+
in: "path",
|
|
3693
|
+
required: true,
|
|
3694
|
+
schema: { type: "string" }
|
|
3695
|
+
}
|
|
3696
|
+
],
|
|
3697
|
+
responses: {
|
|
3698
|
+
204: {
|
|
3699
|
+
description: "Resource deleted successfully"
|
|
3700
|
+
},
|
|
3701
|
+
404: {
|
|
3702
|
+
description: "Resource not found",
|
|
3703
|
+
content: {
|
|
3704
|
+
"application/json": {
|
|
3705
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
},
|
|
3710
|
+
security: security.length > 0 ? security : void 0
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
if (methods.includes("HEAD")) {
|
|
3714
|
+
if (!paths[basePath]) paths[basePath] = {};
|
|
3715
|
+
paths[basePath].head = {
|
|
3716
|
+
tags: [resourceName],
|
|
3717
|
+
summary: `Get ${resourceName} statistics`,
|
|
3718
|
+
description: `Get statistics about ${resourceName} collection without retrieving data. Returns statistics in response headers.`,
|
|
3719
|
+
responses: {
|
|
3720
|
+
200: {
|
|
3721
|
+
description: "Statistics retrieved successfully",
|
|
3722
|
+
headers: {
|
|
3723
|
+
"X-Total-Count": {
|
|
3724
|
+
description: "Total number of records",
|
|
3725
|
+
schema: { type: "integer" }
|
|
3726
|
+
},
|
|
3727
|
+
"X-Resource-Version": {
|
|
3728
|
+
description: "Current resource version",
|
|
3729
|
+
schema: { type: "string" }
|
|
3730
|
+
},
|
|
3731
|
+
"X-Schema-Fields": {
|
|
3732
|
+
description: "Number of schema fields",
|
|
3733
|
+
schema: { type: "integer" }
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
},
|
|
3738
|
+
security: security.length > 0 ? security : void 0
|
|
3739
|
+
};
|
|
3740
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3741
|
+
paths[`${basePath}/{id}`].head = {
|
|
3742
|
+
tags: [resourceName],
|
|
3743
|
+
summary: `Check if ${resourceName} exists`,
|
|
3744
|
+
description: `Check if a ${resourceName} exists without retrieving its data`,
|
|
3745
|
+
parameters: [
|
|
3746
|
+
{
|
|
3747
|
+
name: "id",
|
|
3748
|
+
in: "path",
|
|
3749
|
+
required: true,
|
|
3750
|
+
schema: { type: "string" }
|
|
3751
|
+
}
|
|
3752
|
+
],
|
|
3753
|
+
responses: {
|
|
3754
|
+
200: {
|
|
3755
|
+
description: "Resource exists",
|
|
3756
|
+
headers: {
|
|
3757
|
+
"Last-Modified": {
|
|
3758
|
+
description: "Last modification date",
|
|
3759
|
+
schema: { type: "string", format: "date-time" }
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
},
|
|
3763
|
+
404: {
|
|
3764
|
+
description: "Resource not found"
|
|
3765
|
+
}
|
|
3766
|
+
},
|
|
3767
|
+
security: security.length > 0 ? security : void 0
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
if (methods.includes("OPTIONS")) {
|
|
3771
|
+
if (!paths[basePath]) paths[basePath] = {};
|
|
3772
|
+
paths[basePath].options = {
|
|
3773
|
+
tags: [resourceName],
|
|
3774
|
+
summary: `Get ${resourceName} metadata`,
|
|
3775
|
+
description: `Get complete metadata about ${resourceName} resource including schema, allowed methods, endpoints, and query parameters`,
|
|
3776
|
+
responses: {
|
|
3777
|
+
200: {
|
|
3778
|
+
description: "Metadata retrieved successfully",
|
|
3779
|
+
headers: {
|
|
3780
|
+
"Allow": {
|
|
3781
|
+
description: "Allowed HTTP methods",
|
|
3782
|
+
schema: { type: "string", example: "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS" }
|
|
3783
|
+
}
|
|
3784
|
+
},
|
|
3785
|
+
content: {
|
|
3786
|
+
"application/json": {
|
|
3787
|
+
schema: {
|
|
3788
|
+
type: "object",
|
|
3789
|
+
properties: {
|
|
3790
|
+
resource: { type: "string" },
|
|
3791
|
+
version: { type: "string" },
|
|
3792
|
+
totalRecords: { type: "integer" },
|
|
3793
|
+
allowedMethods: {
|
|
3794
|
+
type: "array",
|
|
3795
|
+
items: { type: "string" }
|
|
3796
|
+
},
|
|
3797
|
+
schema: {
|
|
3798
|
+
type: "array",
|
|
3799
|
+
items: {
|
|
3800
|
+
type: "object",
|
|
3801
|
+
properties: {
|
|
3802
|
+
name: { type: "string" },
|
|
3803
|
+
type: { type: "string" },
|
|
3804
|
+
rules: { type: "array", items: { type: "string" } }
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
},
|
|
3808
|
+
endpoints: {
|
|
3809
|
+
type: "object",
|
|
3810
|
+
properties: {
|
|
3811
|
+
list: { type: "string" },
|
|
3812
|
+
get: { type: "string" },
|
|
3813
|
+
create: { type: "string" },
|
|
3814
|
+
update: { type: "string" },
|
|
3815
|
+
delete: { type: "string" }
|
|
3816
|
+
}
|
|
3817
|
+
},
|
|
3818
|
+
queryParameters: { type: "object" }
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
};
|
|
3826
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3827
|
+
paths[`${basePath}/{id}`].options = {
|
|
3828
|
+
tags: [resourceName],
|
|
3829
|
+
summary: `Get allowed methods for ${resourceName} item`,
|
|
3830
|
+
description: `Get allowed HTTP methods for individual ${resourceName} operations`,
|
|
3831
|
+
parameters: [
|
|
3832
|
+
{
|
|
3833
|
+
name: "id",
|
|
3834
|
+
in: "path",
|
|
3835
|
+
required: true,
|
|
3836
|
+
schema: { type: "string" }
|
|
3837
|
+
}
|
|
3838
|
+
],
|
|
3839
|
+
responses: {
|
|
3840
|
+
204: {
|
|
3841
|
+
description: "Methods retrieved successfully",
|
|
3842
|
+
headers: {
|
|
3843
|
+
"Allow": {
|
|
3844
|
+
description: "Allowed HTTP methods",
|
|
3845
|
+
schema: { type: "string", example: "GET, PUT, PATCH, DELETE, HEAD, OPTIONS" }
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
return paths;
|
|
3853
|
+
}
|
|
3854
|
+
function generateRelationalPaths(resource, relationName, relationConfig, version, relatedSchema) {
|
|
3855
|
+
const resourceName = resource.name;
|
|
3856
|
+
const basePath = `/${version}/${resourceName}/{id}/${relationName}`;
|
|
3857
|
+
relationConfig.resource;
|
|
3858
|
+
const isToMany = relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany";
|
|
3859
|
+
const paths = {};
|
|
3860
|
+
paths[basePath] = {
|
|
3861
|
+
get: {
|
|
3862
|
+
tags: [resourceName],
|
|
3863
|
+
summary: `Get ${relationName} of ${resourceName}`,
|
|
3864
|
+
description: `Retrieve ${relationName} (${relationConfig.type}) associated with this ${resourceName}. This endpoint uses the RelationPlugin to efficiently load related data` + (relationConfig.partitionHint ? ` via the '${relationConfig.partitionHint}' partition.` : "."),
|
|
3865
|
+
parameters: [
|
|
3866
|
+
{
|
|
3867
|
+
name: "id",
|
|
3868
|
+
in: "path",
|
|
3869
|
+
required: true,
|
|
3870
|
+
description: `${resourceName} ID`,
|
|
3871
|
+
schema: { type: "string" }
|
|
3872
|
+
},
|
|
3873
|
+
...isToMany ? [
|
|
3874
|
+
{
|
|
3875
|
+
name: "limit",
|
|
3876
|
+
in: "query",
|
|
3877
|
+
description: "Maximum number of items to return",
|
|
3878
|
+
schema: { type: "integer", default: 100, minimum: 1, maximum: 1e3 }
|
|
3879
|
+
},
|
|
3880
|
+
{
|
|
3881
|
+
name: "offset",
|
|
3882
|
+
in: "query",
|
|
3883
|
+
description: "Number of items to skip",
|
|
3884
|
+
schema: { type: "integer", default: 0, minimum: 0 }
|
|
3885
|
+
}
|
|
3886
|
+
] : []
|
|
3887
|
+
],
|
|
3888
|
+
responses: {
|
|
3889
|
+
200: {
|
|
3890
|
+
description: "Successful response",
|
|
3891
|
+
content: {
|
|
3892
|
+
"application/json": {
|
|
3893
|
+
schema: isToMany ? {
|
|
3894
|
+
type: "object",
|
|
3895
|
+
properties: {
|
|
3896
|
+
success: { type: "boolean", example: true },
|
|
3897
|
+
data: {
|
|
3898
|
+
type: "array",
|
|
3899
|
+
items: relatedSchema
|
|
3900
|
+
},
|
|
3901
|
+
pagination: {
|
|
3902
|
+
type: "object",
|
|
3903
|
+
properties: {
|
|
3904
|
+
total: { type: "integer" },
|
|
3905
|
+
page: { type: "integer" },
|
|
3906
|
+
pageSize: { type: "integer" },
|
|
3907
|
+
pageCount: { type: "integer" }
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
} : {
|
|
3912
|
+
type: "object",
|
|
3913
|
+
properties: {
|
|
3914
|
+
success: { type: "boolean", example: true },
|
|
3915
|
+
data: relatedSchema
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
},
|
|
3920
|
+
...isToMany ? {
|
|
3921
|
+
headers: {
|
|
3922
|
+
"X-Total-Count": {
|
|
3923
|
+
description: "Total number of related records",
|
|
3924
|
+
schema: { type: "integer" }
|
|
3925
|
+
},
|
|
3926
|
+
"X-Page-Count": {
|
|
3927
|
+
description: "Total number of pages",
|
|
3928
|
+
schema: { type: "integer" }
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
} : {}
|
|
3932
|
+
},
|
|
3933
|
+
404: {
|
|
3934
|
+
description: `${resourceName} not found` + (isToMany ? "" : " or no related resource exists"),
|
|
3935
|
+
content: {
|
|
3936
|
+
"application/json": {
|
|
3937
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
};
|
|
3944
|
+
return paths;
|
|
3945
|
+
}
|
|
3946
|
+
function generateOpenAPISpec(database, config = {}) {
|
|
3947
|
+
const {
|
|
3948
|
+
title = "s3db.js API",
|
|
3949
|
+
version = "1.0.0",
|
|
3950
|
+
description = "Auto-generated REST API documentation for s3db.js resources",
|
|
3951
|
+
serverUrl = "http://localhost:3000",
|
|
3952
|
+
auth = {},
|
|
3953
|
+
resources: resourceConfigs = {}
|
|
3954
|
+
} = config;
|
|
3955
|
+
const resourcesTableRows = [];
|
|
3956
|
+
for (const [name, resource] of Object.entries(database.resources)) {
|
|
3957
|
+
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
3958
|
+
continue;
|
|
3959
|
+
}
|
|
3960
|
+
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
3961
|
+
const resourceDescription = resource.config?.description;
|
|
3962
|
+
const descText = typeof resourceDescription === "object" ? resourceDescription.resource : resourceDescription || "No description";
|
|
3963
|
+
resourcesTableRows.push(`| ${name} | ${descText} | \`/${version2}/${name}\` |`);
|
|
3964
|
+
}
|
|
3965
|
+
const enhancedDescription = `${description}
|
|
3966
|
+
|
|
3967
|
+
## Available Resources
|
|
3968
|
+
|
|
3969
|
+
| Resource | Description | Base Path |
|
|
3970
|
+
|----------|-------------|-----------|
|
|
3971
|
+
${resourcesTableRows.join("\n")}
|
|
3972
|
+
|
|
3973
|
+
---
|
|
3974
|
+
|
|
3975
|
+
For detailed information about each endpoint, see the sections below.`;
|
|
3976
|
+
const spec = {
|
|
3977
|
+
openapi: "3.1.0",
|
|
3978
|
+
info: {
|
|
3979
|
+
title,
|
|
3980
|
+
version,
|
|
3981
|
+
description: enhancedDescription,
|
|
3982
|
+
contact: {
|
|
3983
|
+
name: "s3db.js",
|
|
3984
|
+
url: "https://github.com/forattini-dev/s3db.js"
|
|
3985
|
+
}
|
|
3986
|
+
},
|
|
3987
|
+
servers: [
|
|
3988
|
+
{
|
|
3989
|
+
url: serverUrl,
|
|
3990
|
+
description: "API Server"
|
|
3991
|
+
}
|
|
3992
|
+
],
|
|
3993
|
+
paths: {},
|
|
3994
|
+
components: {
|
|
3995
|
+
schemas: {
|
|
3996
|
+
Error: {
|
|
3997
|
+
type: "object",
|
|
3998
|
+
properties: {
|
|
3999
|
+
success: { type: "boolean", example: false },
|
|
4000
|
+
error: {
|
|
4001
|
+
type: "object",
|
|
4002
|
+
properties: {
|
|
4003
|
+
message: { type: "string" },
|
|
4004
|
+
code: { type: "string" },
|
|
4005
|
+
details: { type: "object" }
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
},
|
|
4010
|
+
ValidationError: {
|
|
4011
|
+
type: "object",
|
|
4012
|
+
properties: {
|
|
4013
|
+
success: { type: "boolean", example: false },
|
|
4014
|
+
error: {
|
|
4015
|
+
type: "object",
|
|
4016
|
+
properties: {
|
|
4017
|
+
message: { type: "string", example: "Validation failed" },
|
|
4018
|
+
code: { type: "string", example: "VALIDATION_ERROR" },
|
|
4019
|
+
details: {
|
|
4020
|
+
type: "object",
|
|
4021
|
+
properties: {
|
|
4022
|
+
errors: {
|
|
4023
|
+
type: "array",
|
|
4024
|
+
items: {
|
|
4025
|
+
type: "object",
|
|
4026
|
+
properties: {
|
|
4027
|
+
field: { type: "string" },
|
|
4028
|
+
message: { type: "string" },
|
|
4029
|
+
expected: { type: "string" },
|
|
4030
|
+
actual: {}
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
},
|
|
4041
|
+
securitySchemes: {}
|
|
4042
|
+
},
|
|
4043
|
+
tags: []
|
|
4044
|
+
};
|
|
4045
|
+
if (auth.jwt?.enabled) {
|
|
4046
|
+
spec.components.securitySchemes.bearerAuth = {
|
|
4047
|
+
type: "http",
|
|
4048
|
+
scheme: "bearer",
|
|
4049
|
+
bearerFormat: "JWT",
|
|
4050
|
+
description: "JWT authentication"
|
|
4051
|
+
};
|
|
4052
|
+
}
|
|
4053
|
+
if (auth.apiKey?.enabled) {
|
|
4054
|
+
spec.components.securitySchemes.apiKeyAuth = {
|
|
4055
|
+
type: "apiKey",
|
|
4056
|
+
in: "header",
|
|
4057
|
+
name: auth.apiKey.headerName || "X-API-Key",
|
|
4058
|
+
description: "API Key authentication"
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
if (auth.basic?.enabled) {
|
|
4062
|
+
spec.components.securitySchemes.basicAuth = {
|
|
4063
|
+
type: "http",
|
|
4064
|
+
scheme: "basic",
|
|
4065
|
+
description: "HTTP Basic authentication"
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
const resources = database.resources;
|
|
4069
|
+
const relationsPlugin = database.plugins?.relation || database.plugins?.RelationPlugin || null;
|
|
4070
|
+
for (const [name, resource] of Object.entries(resources)) {
|
|
4071
|
+
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
4072
|
+
continue;
|
|
4073
|
+
}
|
|
4074
|
+
const config2 = resourceConfigs[name] || {
|
|
4075
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
4076
|
+
auth: false
|
|
4077
|
+
};
|
|
4078
|
+
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
4079
|
+
const paths = generateResourcePaths(resource, version2, config2);
|
|
4080
|
+
Object.assign(spec.paths, paths);
|
|
4081
|
+
const resourceDescription = resource.config?.description;
|
|
4082
|
+
const tagDescription = typeof resourceDescription === "object" ? resourceDescription.resource : resourceDescription || `Operations for ${name} resource`;
|
|
4083
|
+
spec.tags.push({
|
|
4084
|
+
name,
|
|
4085
|
+
description: tagDescription
|
|
4086
|
+
});
|
|
4087
|
+
spec.components.schemas[name] = generateResourceSchema(resource);
|
|
4088
|
+
if (relationsPlugin && relationsPlugin.relations && relationsPlugin.relations[name]) {
|
|
4089
|
+
const relationsDef = relationsPlugin.relations[name];
|
|
4090
|
+
for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
|
|
4091
|
+
if (relationConfig.type === "belongsTo") {
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
4094
|
+
const exposeRelation = config2?.relations?.[relationName]?.expose !== false;
|
|
4095
|
+
if (!exposeRelation) {
|
|
4096
|
+
continue;
|
|
4097
|
+
}
|
|
4098
|
+
const relatedResource = database.resources[relationConfig.resource];
|
|
4099
|
+
if (!relatedResource) {
|
|
4100
|
+
continue;
|
|
4101
|
+
}
|
|
4102
|
+
const relatedSchema = generateResourceSchema(relatedResource);
|
|
4103
|
+
const relationalPaths = generateRelationalPaths(
|
|
4104
|
+
resource,
|
|
4105
|
+
relationName,
|
|
4106
|
+
relationConfig,
|
|
4107
|
+
version2,
|
|
4108
|
+
relatedSchema
|
|
4109
|
+
);
|
|
4110
|
+
Object.assign(spec.paths, relationalPaths);
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
if (auth.jwt?.enabled || auth.apiKey?.enabled || auth.basic?.enabled) {
|
|
4115
|
+
spec.paths["/auth/login"] = {
|
|
4116
|
+
post: {
|
|
4117
|
+
tags: ["Authentication"],
|
|
4118
|
+
summary: "Login",
|
|
4119
|
+
description: "Authenticate with username and password",
|
|
4120
|
+
requestBody: {
|
|
4121
|
+
required: true,
|
|
4122
|
+
content: {
|
|
4123
|
+
"application/json": {
|
|
4124
|
+
schema: {
|
|
4125
|
+
type: "object",
|
|
4126
|
+
properties: {
|
|
4127
|
+
username: { type: "string" },
|
|
4128
|
+
password: { type: "string", format: "password" }
|
|
4129
|
+
},
|
|
4130
|
+
required: ["username", "password"]
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
},
|
|
4135
|
+
responses: {
|
|
4136
|
+
200: {
|
|
4137
|
+
description: "Login successful",
|
|
4138
|
+
content: {
|
|
4139
|
+
"application/json": {
|
|
4140
|
+
schema: {
|
|
4141
|
+
type: "object",
|
|
4142
|
+
properties: {
|
|
4143
|
+
success: { type: "boolean", example: true },
|
|
4144
|
+
data: {
|
|
4145
|
+
type: "object",
|
|
4146
|
+
properties: {
|
|
4147
|
+
token: { type: "string" },
|
|
4148
|
+
user: { type: "object" }
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
},
|
|
4156
|
+
401: {
|
|
4157
|
+
description: "Invalid credentials",
|
|
4158
|
+
content: {
|
|
4159
|
+
"application/json": {
|
|
4160
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
};
|
|
4167
|
+
spec.paths["/auth/register"] = {
|
|
4168
|
+
post: {
|
|
4169
|
+
tags: ["Authentication"],
|
|
4170
|
+
summary: "Register",
|
|
4171
|
+
description: "Register a new user",
|
|
4172
|
+
requestBody: {
|
|
4173
|
+
required: true,
|
|
4174
|
+
content: {
|
|
4175
|
+
"application/json": {
|
|
4176
|
+
schema: {
|
|
4177
|
+
type: "object",
|
|
4178
|
+
properties: {
|
|
4179
|
+
username: { type: "string", minLength: 3 },
|
|
4180
|
+
password: { type: "string", format: "password", minLength: 8 },
|
|
4181
|
+
email: { type: "string", format: "email" }
|
|
4182
|
+
},
|
|
4183
|
+
required: ["username", "password"]
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
},
|
|
4188
|
+
responses: {
|
|
4189
|
+
201: {
|
|
4190
|
+
description: "User registered successfully",
|
|
4191
|
+
content: {
|
|
4192
|
+
"application/json": {
|
|
4193
|
+
schema: {
|
|
4194
|
+
type: "object",
|
|
4195
|
+
properties: {
|
|
4196
|
+
success: { type: "boolean", example: true },
|
|
4197
|
+
data: {
|
|
4198
|
+
type: "object",
|
|
4199
|
+
properties: {
|
|
4200
|
+
token: { type: "string" },
|
|
4201
|
+
user: { type: "object" }
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
};
|
|
4212
|
+
spec.tags.push({
|
|
4213
|
+
name: "Authentication",
|
|
4214
|
+
description: "Authentication endpoints"
|
|
4215
|
+
});
|
|
4216
|
+
}
|
|
4217
|
+
spec.paths["/health"] = {
|
|
4218
|
+
get: {
|
|
4219
|
+
tags: ["Health"],
|
|
4220
|
+
summary: "Generic Health Check",
|
|
4221
|
+
description: "Generic health check endpoint that includes references to liveness and readiness probes",
|
|
4222
|
+
responses: {
|
|
4223
|
+
200: {
|
|
4224
|
+
description: "API is healthy",
|
|
4225
|
+
content: {
|
|
4226
|
+
"application/json": {
|
|
4227
|
+
schema: {
|
|
4228
|
+
type: "object",
|
|
4229
|
+
properties: {
|
|
4230
|
+
success: { type: "boolean", example: true },
|
|
4231
|
+
data: {
|
|
4232
|
+
type: "object",
|
|
4233
|
+
properties: {
|
|
4234
|
+
status: { type: "string", example: "ok" },
|
|
4235
|
+
uptime: { type: "number", description: "Process uptime in seconds" },
|
|
4236
|
+
timestamp: { type: "string", format: "date-time" },
|
|
4237
|
+
checks: {
|
|
4238
|
+
type: "object",
|
|
4239
|
+
properties: {
|
|
4240
|
+
liveness: { type: "string", example: "/health/live" },
|
|
4241
|
+
readiness: { type: "string", example: "/health/ready" }
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
};
|
|
4254
|
+
spec.paths["/health/live"] = {
|
|
4255
|
+
get: {
|
|
4256
|
+
tags: ["Health"],
|
|
4257
|
+
summary: "Liveness Probe",
|
|
4258
|
+
description: "Kubernetes liveness probe - checks if the application is alive. If this fails, Kubernetes will restart the pod.",
|
|
4259
|
+
responses: {
|
|
4260
|
+
200: {
|
|
4261
|
+
description: "Application is alive",
|
|
4262
|
+
content: {
|
|
4263
|
+
"application/json": {
|
|
4264
|
+
schema: {
|
|
4265
|
+
type: "object",
|
|
4266
|
+
properties: {
|
|
4267
|
+
success: { type: "boolean", example: true },
|
|
4268
|
+
data: {
|
|
4269
|
+
type: "object",
|
|
4270
|
+
properties: {
|
|
4271
|
+
status: { type: "string", example: "alive" },
|
|
4272
|
+
timestamp: { type: "string", format: "date-time" }
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
};
|
|
4283
|
+
spec.paths["/health/ready"] = {
|
|
4284
|
+
get: {
|
|
4285
|
+
tags: ["Health"],
|
|
4286
|
+
summary: "Readiness Probe",
|
|
4287
|
+
description: "Kubernetes readiness probe - checks if the application is ready to receive traffic. If this fails, Kubernetes will remove the pod from service endpoints.",
|
|
4288
|
+
responses: {
|
|
4289
|
+
200: {
|
|
4290
|
+
description: "Application is ready to receive traffic",
|
|
4291
|
+
content: {
|
|
4292
|
+
"application/json": {
|
|
4293
|
+
schema: {
|
|
4294
|
+
type: "object",
|
|
4295
|
+
properties: {
|
|
4296
|
+
success: { type: "boolean", example: true },
|
|
4297
|
+
data: {
|
|
4298
|
+
type: "object",
|
|
4299
|
+
properties: {
|
|
4300
|
+
status: { type: "string", example: "ready" },
|
|
4301
|
+
database: {
|
|
4302
|
+
type: "object",
|
|
4303
|
+
properties: {
|
|
4304
|
+
connected: { type: "boolean", example: true },
|
|
4305
|
+
resources: { type: "integer", example: 5 }
|
|
4306
|
+
}
|
|
4307
|
+
},
|
|
4308
|
+
timestamp: { type: "string", format: "date-time" }
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
},
|
|
4316
|
+
503: {
|
|
4317
|
+
description: "Application is not ready",
|
|
4318
|
+
content: {
|
|
4319
|
+
"application/json": {
|
|
4320
|
+
schema: {
|
|
4321
|
+
type: "object",
|
|
4322
|
+
properties: {
|
|
4323
|
+
success: { type: "boolean", example: false },
|
|
4324
|
+
error: {
|
|
4325
|
+
type: "object",
|
|
4326
|
+
properties: {
|
|
4327
|
+
message: { type: "string", example: "Service not ready" },
|
|
4328
|
+
code: { type: "string", example: "NOT_READY" },
|
|
4329
|
+
details: {
|
|
4330
|
+
type: "object",
|
|
4331
|
+
properties: {
|
|
4332
|
+
database: {
|
|
4333
|
+
type: "object",
|
|
4334
|
+
properties: {
|
|
4335
|
+
connected: { type: "boolean", example: false },
|
|
4336
|
+
resources: { type: "integer", example: 0 }
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
};
|
|
4351
|
+
spec.tags.push({
|
|
4352
|
+
name: "Health",
|
|
4353
|
+
description: "Health check endpoints for monitoring and Kubernetes probes"
|
|
4354
|
+
});
|
|
4355
|
+
const metricsPlugin = database.plugins?.metrics || database.plugins?.MetricsPlugin;
|
|
4356
|
+
if (metricsPlugin && metricsPlugin.config?.prometheus?.enabled) {
|
|
4357
|
+
const metricsPath = metricsPlugin.config.prometheus.path || "/metrics";
|
|
4358
|
+
const isIntegrated = metricsPlugin.config.prometheus.mode !== "standalone";
|
|
4359
|
+
if (isIntegrated) {
|
|
4360
|
+
spec.paths[metricsPath] = {
|
|
4361
|
+
get: {
|
|
4362
|
+
tags: ["Monitoring"],
|
|
4363
|
+
summary: "Prometheus Metrics",
|
|
4364
|
+
description: "Exposes application metrics in Prometheus text-based exposition format for monitoring and observability. Metrics include operation counts, durations, errors, uptime, and resource statistics.",
|
|
4365
|
+
responses: {
|
|
4366
|
+
200: {
|
|
4367
|
+
description: "Metrics in Prometheus format",
|
|
4368
|
+
content: {
|
|
4369
|
+
"text/plain": {
|
|
4370
|
+
schema: {
|
|
4371
|
+
type: "string",
|
|
4372
|
+
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'
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
};
|
|
4380
|
+
spec.tags.push({
|
|
4381
|
+
name: "Monitoring",
|
|
4382
|
+
description: "Monitoring and observability endpoints (Prometheus)"
|
|
4383
|
+
});
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
return spec;
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
class ApiServer {
|
|
4390
|
+
/**
|
|
4391
|
+
* Create API server
|
|
4392
|
+
* @param {Object} options - Server options
|
|
4393
|
+
* @param {number} options.port - Server port
|
|
4394
|
+
* @param {string} options.host - Server host
|
|
4395
|
+
* @param {Object} options.database - s3db.js database instance
|
|
4396
|
+
* @param {Object} options.resources - Resource configuration
|
|
4397
|
+
* @param {Array} options.middlewares - Global middlewares
|
|
4398
|
+
*/
|
|
4399
|
+
constructor(options = {}) {
|
|
4400
|
+
this.options = {
|
|
4401
|
+
port: options.port || 3e3,
|
|
4402
|
+
host: options.host || "0.0.0.0",
|
|
4403
|
+
database: options.database,
|
|
4404
|
+
resources: options.resources || {},
|
|
4405
|
+
middlewares: options.middlewares || [],
|
|
4406
|
+
verbose: options.verbose || false,
|
|
4407
|
+
auth: options.auth || {},
|
|
4408
|
+
docsEnabled: options.docsEnabled !== false,
|
|
4409
|
+
// Enable /docs by default
|
|
4410
|
+
docsUI: options.docsUI || "redoc",
|
|
4411
|
+
// 'swagger' or 'redoc'
|
|
4412
|
+
maxBodySize: options.maxBodySize || 10 * 1024 * 1024,
|
|
4413
|
+
// 10MB default
|
|
4414
|
+
rootHandler: options.rootHandler,
|
|
4415
|
+
// Custom handler for root path, if not provided redirects to /docs
|
|
4416
|
+
apiInfo: {
|
|
4417
|
+
title: options.apiTitle || "s3db.js API",
|
|
4418
|
+
version: options.apiVersion || "1.0.0",
|
|
4419
|
+
description: options.apiDescription || "Auto-generated REST API for s3db.js resources"
|
|
4420
|
+
}
|
|
4421
|
+
};
|
|
4422
|
+
this.app = null;
|
|
4423
|
+
this.server = null;
|
|
4424
|
+
this.isRunning = false;
|
|
4425
|
+
this.openAPISpec = null;
|
|
4426
|
+
this.initialized = false;
|
|
4427
|
+
this.relationsPlugin = this.options.database?.plugins?.relation || this.options.database?.plugins?.RelationPlugin || null;
|
|
4428
|
+
}
|
|
4429
|
+
/**
|
|
4430
|
+
* Setup all routes
|
|
4431
|
+
* @private
|
|
4432
|
+
*/
|
|
4433
|
+
_setupRoutes() {
|
|
4434
|
+
this.options.middlewares.forEach((middleware) => {
|
|
4435
|
+
this.app.use("*", middleware);
|
|
4436
|
+
});
|
|
4437
|
+
this.app.use("*", async (c, next) => {
|
|
4438
|
+
const method = c.req.method;
|
|
4439
|
+
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
4440
|
+
const contentLength = c.req.header("content-length");
|
|
4441
|
+
if (contentLength) {
|
|
4442
|
+
const size = parseInt(contentLength);
|
|
4443
|
+
if (size > this.options.maxBodySize) {
|
|
4444
|
+
const response = payloadTooLarge(size, this.options.maxBodySize);
|
|
4445
|
+
c.header("Connection", "close");
|
|
4446
|
+
return c.json(response, response._status);
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
await next();
|
|
4451
|
+
});
|
|
4452
|
+
this.app.get("/health/live", (c) => {
|
|
4453
|
+
const response = success({
|
|
4454
|
+
status: "alive",
|
|
4455
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4456
|
+
});
|
|
4457
|
+
return c.json(response);
|
|
4458
|
+
});
|
|
4459
|
+
this.app.get("/health/ready", (c) => {
|
|
4460
|
+
const isReady = this.options.database && this.options.database.connected && Object.keys(this.options.database.resources).length > 0;
|
|
4461
|
+
if (!isReady) {
|
|
4462
|
+
const response2 = error("Service not ready", {
|
|
4463
|
+
status: 503,
|
|
4464
|
+
code: "NOT_READY",
|
|
4465
|
+
details: {
|
|
4466
|
+
database: {
|
|
4467
|
+
connected: this.options.database?.connected || false,
|
|
4468
|
+
resources: Object.keys(this.options.database?.resources || {}).length
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
});
|
|
4472
|
+
return c.json(response2, 503);
|
|
4473
|
+
}
|
|
4474
|
+
const response = success({
|
|
4475
|
+
status: "ready",
|
|
4476
|
+
database: {
|
|
4477
|
+
connected: true,
|
|
4478
|
+
resources: Object.keys(this.options.database.resources).length
|
|
4479
|
+
},
|
|
4480
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4481
|
+
});
|
|
4482
|
+
return c.json(response);
|
|
4483
|
+
});
|
|
4484
|
+
this.app.get("/health", (c) => {
|
|
4485
|
+
const response = success({
|
|
4486
|
+
status: "ok",
|
|
4487
|
+
uptime: process.uptime(),
|
|
4488
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4489
|
+
checks: {
|
|
4490
|
+
liveness: "/health/live",
|
|
4491
|
+
readiness: "/health/ready"
|
|
4492
|
+
}
|
|
4493
|
+
});
|
|
4494
|
+
return c.json(response);
|
|
4495
|
+
});
|
|
4496
|
+
this.app.get("/", (c) => {
|
|
4497
|
+
if (this.options.rootHandler) {
|
|
4498
|
+
return this.options.rootHandler(c);
|
|
4499
|
+
}
|
|
4500
|
+
return c.redirect("/docs", 302);
|
|
4501
|
+
});
|
|
4502
|
+
if (this.options.docsEnabled) {
|
|
4503
|
+
this.app.get("/openapi.json", (c) => {
|
|
4504
|
+
if (!this.openAPISpec) {
|
|
4505
|
+
this.openAPISpec = this._generateOpenAPISpec();
|
|
4506
|
+
}
|
|
4507
|
+
return c.json(this.openAPISpec);
|
|
4508
|
+
});
|
|
4509
|
+
if (this.options.docsUI === "swagger") {
|
|
4510
|
+
this.app.get("/docs", this.swaggerUI({
|
|
4511
|
+
url: "/openapi.json"
|
|
4512
|
+
}));
|
|
4513
|
+
} else {
|
|
4514
|
+
this.app.get("/docs", (c) => {
|
|
4515
|
+
return c.html(`<!DOCTYPE html>
|
|
4516
|
+
<html lang="en">
|
|
4517
|
+
<head>
|
|
4518
|
+
<meta charset="UTF-8">
|
|
4519
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4520
|
+
<title>${this.options.apiInfo.title} - API Documentation</title>
|
|
4521
|
+
<style>
|
|
4522
|
+
body {
|
|
4523
|
+
margin: 0;
|
|
4524
|
+
padding: 0;
|
|
4525
|
+
}
|
|
4526
|
+
</style>
|
|
4527
|
+
</head>
|
|
4528
|
+
<body>
|
|
4529
|
+
<redoc spec-url="/openapi.json"></redoc>
|
|
4530
|
+
<script src="https://cdn.redoc.ly/redoc/v2.5.1/bundles/redoc.standalone.js"><\/script>
|
|
4531
|
+
</body>
|
|
4532
|
+
</html>`);
|
|
4533
|
+
});
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
this._setupResourceRoutes();
|
|
4537
|
+
if (this.relationsPlugin) {
|
|
4538
|
+
this._setupRelationalRoutes();
|
|
4539
|
+
}
|
|
4540
|
+
this.app.onError((err, c) => {
|
|
4541
|
+
return errorHandler(err, c);
|
|
4542
|
+
});
|
|
4543
|
+
this.app.notFound((c) => {
|
|
4544
|
+
const response = error("Route not found", {
|
|
4545
|
+
status: 404,
|
|
4546
|
+
code: "NOT_FOUND",
|
|
4547
|
+
details: {
|
|
4548
|
+
path: c.req.path,
|
|
4549
|
+
method: c.req.method
|
|
4550
|
+
}
|
|
4551
|
+
});
|
|
4552
|
+
return c.json(response, 404);
|
|
4553
|
+
});
|
|
4554
|
+
}
|
|
4555
|
+
/**
|
|
4556
|
+
* Setup routes for all resources
|
|
4557
|
+
* @private
|
|
4558
|
+
*/
|
|
4559
|
+
_setupResourceRoutes() {
|
|
4560
|
+
const { database, resources: resourceConfigs } = this.options;
|
|
4561
|
+
const resources = database.resources;
|
|
4562
|
+
for (const [name, resource] of Object.entries(resources)) {
|
|
4563
|
+
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
4564
|
+
continue;
|
|
4565
|
+
}
|
|
4566
|
+
const config = resourceConfigs[name] || {
|
|
4567
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]};
|
|
4568
|
+
const version = resource.config?.currentVersion || resource.version || "v1";
|
|
4569
|
+
const resourceApp = createResourceRoutes(resource, version, {
|
|
4570
|
+
methods: config.methods,
|
|
4571
|
+
customMiddleware: config.customMiddleware || [],
|
|
4572
|
+
enableValidation: config.validation !== false
|
|
4573
|
+
}, this.Hono);
|
|
4574
|
+
this.app.route(`/${version}/${name}`, resourceApp);
|
|
4575
|
+
if (this.options.verbose) {
|
|
4576
|
+
console.log(`[API Plugin] Mounted routes for resource '${name}' at /${version}/${name}`);
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
/**
|
|
4581
|
+
* Setup relational routes (when RelationPlugin is active)
|
|
4582
|
+
* @private
|
|
4583
|
+
*/
|
|
4584
|
+
_setupRelationalRoutes() {
|
|
4585
|
+
if (!this.relationsPlugin || !this.relationsPlugin.relations) {
|
|
4586
|
+
return;
|
|
4587
|
+
}
|
|
4588
|
+
const { database } = this.options;
|
|
4589
|
+
const relations = this.relationsPlugin.relations;
|
|
4590
|
+
if (this.options.verbose) {
|
|
4591
|
+
console.log("[API Plugin] Setting up relational routes...");
|
|
4592
|
+
}
|
|
4593
|
+
for (const [resourceName, relationsDef] of Object.entries(relations)) {
|
|
4594
|
+
const resource = database.resources[resourceName];
|
|
4595
|
+
if (!resource) {
|
|
4596
|
+
if (this.options.verbose) {
|
|
4597
|
+
console.warn(`[API Plugin] Resource '${resourceName}' not found for relational routes`);
|
|
4598
|
+
}
|
|
4599
|
+
continue;
|
|
4600
|
+
}
|
|
4601
|
+
if (resourceName.startsWith("plg_") && !this.options.resources[resourceName]) {
|
|
4602
|
+
continue;
|
|
4603
|
+
}
|
|
4604
|
+
const version = resource.config?.currentVersion || resource.version || "v1";
|
|
4605
|
+
for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
|
|
4606
|
+
if (relationConfig.type === "belongsTo") {
|
|
4607
|
+
continue;
|
|
4608
|
+
}
|
|
4609
|
+
const resourceConfig = this.options.resources[resourceName];
|
|
4610
|
+
const exposeRelation = resourceConfig?.relations?.[relationName]?.expose !== false;
|
|
4611
|
+
if (!exposeRelation) {
|
|
4612
|
+
continue;
|
|
4613
|
+
}
|
|
4614
|
+
const relationalApp = createRelationalRoutes(
|
|
4615
|
+
resource,
|
|
4616
|
+
relationName,
|
|
4617
|
+
relationConfig,
|
|
4618
|
+
version,
|
|
4619
|
+
this.Hono
|
|
4620
|
+
);
|
|
4621
|
+
this.app.route(`/${version}/${resourceName}/:id/${relationName}`, relationalApp);
|
|
4622
|
+
if (this.options.verbose) {
|
|
4623
|
+
console.log(
|
|
4624
|
+
`[API Plugin] Mounted relational route: /${version}/${resourceName}/:id/${relationName} (${relationConfig.type} -> ${relationConfig.resource})`
|
|
4625
|
+
);
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
/**
|
|
4631
|
+
* Start the server
|
|
4632
|
+
* @returns {Promise<void>}
|
|
4633
|
+
*/
|
|
4634
|
+
async start() {
|
|
4635
|
+
if (this.isRunning) {
|
|
4636
|
+
console.warn("[API Plugin] Server is already running");
|
|
4637
|
+
return;
|
|
4638
|
+
}
|
|
4639
|
+
if (!this.initialized) {
|
|
4640
|
+
const { Hono } = await import('hono');
|
|
4641
|
+
const { serve } = await import('@hono/node-server');
|
|
4642
|
+
const { swaggerUI } = await import('@hono/swagger-ui');
|
|
4643
|
+
this.Hono = Hono;
|
|
4644
|
+
this.serve = serve;
|
|
4645
|
+
this.swaggerUI = swaggerUI;
|
|
4646
|
+
this.app = new Hono();
|
|
4647
|
+
this._setupRoutes();
|
|
4648
|
+
this.initialized = true;
|
|
4649
|
+
}
|
|
4650
|
+
const { port, host } = this.options;
|
|
4651
|
+
return new Promise((resolve, reject) => {
|
|
4652
|
+
try {
|
|
4653
|
+
this.server = this.serve({
|
|
4654
|
+
fetch: this.app.fetch,
|
|
4655
|
+
port,
|
|
4656
|
+
hostname: host
|
|
4657
|
+
}, (info) => {
|
|
4658
|
+
this.isRunning = true;
|
|
4659
|
+
console.log(`[API Plugin] Server listening on http://${info.address}:${info.port}`);
|
|
4660
|
+
resolve();
|
|
4661
|
+
});
|
|
4662
|
+
} catch (err) {
|
|
4663
|
+
reject(err);
|
|
4664
|
+
}
|
|
4665
|
+
});
|
|
4666
|
+
}
|
|
4667
|
+
/**
|
|
4668
|
+
* Stop the server
|
|
4669
|
+
* @returns {Promise<void>}
|
|
4670
|
+
*/
|
|
4671
|
+
async stop() {
|
|
4672
|
+
if (!this.isRunning) {
|
|
4673
|
+
console.warn("[API Plugin] Server is not running");
|
|
4674
|
+
return;
|
|
4675
|
+
}
|
|
4676
|
+
if (this.server && typeof this.server.close === "function") {
|
|
4677
|
+
await new Promise((resolve) => {
|
|
4678
|
+
this.server.close(() => {
|
|
4679
|
+
this.isRunning = false;
|
|
4680
|
+
console.log("[API Plugin] Server stopped");
|
|
4681
|
+
resolve();
|
|
4682
|
+
});
|
|
4683
|
+
});
|
|
4684
|
+
} else {
|
|
4685
|
+
this.isRunning = false;
|
|
4686
|
+
console.log("[API Plugin] Server stopped");
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
/**
|
|
4690
|
+
* Get server info
|
|
4691
|
+
* @returns {Object} Server information
|
|
4692
|
+
*/
|
|
4693
|
+
getInfo() {
|
|
4694
|
+
return {
|
|
4695
|
+
isRunning: this.isRunning,
|
|
4696
|
+
port: this.options.port,
|
|
4697
|
+
host: this.options.host,
|
|
4698
|
+
resources: Object.keys(this.options.database.resources).length
|
|
4699
|
+
};
|
|
4700
|
+
}
|
|
4701
|
+
/**
|
|
4702
|
+
* Get Hono app instance
|
|
4703
|
+
* @returns {Hono} Hono app
|
|
4704
|
+
*/
|
|
4705
|
+
getApp() {
|
|
4706
|
+
return this.app;
|
|
4707
|
+
}
|
|
4708
|
+
/**
|
|
4709
|
+
* Generate OpenAPI specification
|
|
4710
|
+
* @private
|
|
4711
|
+
* @returns {Object} OpenAPI spec
|
|
4712
|
+
*/
|
|
4713
|
+
_generateOpenAPISpec() {
|
|
4714
|
+
const { port, host, database, resources, auth, apiInfo } = this.options;
|
|
4715
|
+
return generateOpenAPISpec(database, {
|
|
4716
|
+
title: apiInfo.title,
|
|
4717
|
+
version: apiInfo.version,
|
|
4718
|
+
description: apiInfo.description,
|
|
4719
|
+
serverUrl: `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`,
|
|
4720
|
+
auth,
|
|
4721
|
+
resources
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
|
|
2683
4726
|
class ApiPlugin extends Plugin {
|
|
2684
4727
|
/**
|
|
2685
4728
|
* Create API Plugin instance
|
|
@@ -3002,11 +5045,6 @@ class ApiPlugin extends Plugin {
|
|
|
3002
5045
|
if (this.config.verbose) {
|
|
3003
5046
|
console.log("[API Plugin] Starting server...");
|
|
3004
5047
|
}
|
|
3005
|
-
const serverPath = "./server.js";
|
|
3006
|
-
const { ApiServer } = await import(
|
|
3007
|
-
/* @vite-ignore */
|
|
3008
|
-
serverPath
|
|
3009
|
-
);
|
|
3010
5048
|
this.server = new ApiServer({
|
|
3011
5049
|
port: this.config.port,
|
|
3012
5050
|
host: this.config.host,
|
|
@@ -4567,7 +6605,7 @@ class BackupPlugin extends Plugin {
|
|
|
4567
6605
|
const storageInfo = this.driver.getStorageInfo();
|
|
4568
6606
|
console.log(`[BackupPlugin] Initialized with driver: ${storageInfo.type}`);
|
|
4569
6607
|
}
|
|
4570
|
-
this.emit("initialized", {
|
|
6608
|
+
this.emit("db:plugin:initialized", {
|
|
4571
6609
|
driver: this.driver.getType(),
|
|
4572
6610
|
config: this.driver.getStorageInfo()
|
|
4573
6611
|
});
|
|
@@ -4615,7 +6653,7 @@ class BackupPlugin extends Plugin {
|
|
|
4615
6653
|
if (this.config.onBackupStart) {
|
|
4616
6654
|
await this._executeHook(this.config.onBackupStart, type, { backupId });
|
|
4617
6655
|
}
|
|
4618
|
-
this.emit("
|
|
6656
|
+
this.emit("plg:backup:start", { id: backupId, type });
|
|
4619
6657
|
const metadata = await this._createBackupMetadata(backupId, type);
|
|
4620
6658
|
const tempBackupDir = path$1.join(this.config.tempDir, backupId);
|
|
4621
6659
|
await mkdir(tempBackupDir, { recursive: true });
|
|
@@ -4648,7 +6686,7 @@ class BackupPlugin extends Plugin {
|
|
|
4648
6686
|
const stats = { backupId, type, size: totalSize, duration, driverInfo: uploadResult };
|
|
4649
6687
|
await this._executeHook(this.config.onBackupComplete, type, stats);
|
|
4650
6688
|
}
|
|
4651
|
-
this.emit("
|
|
6689
|
+
this.emit("plg:backup:complete", {
|
|
4652
6690
|
id: backupId,
|
|
4653
6691
|
type,
|
|
4654
6692
|
size: totalSize,
|
|
@@ -4676,7 +6714,7 @@ class BackupPlugin extends Plugin {
|
|
|
4676
6714
|
error: error.message,
|
|
4677
6715
|
duration: Date.now() - startTime
|
|
4678
6716
|
});
|
|
4679
|
-
this.emit("
|
|
6717
|
+
this.emit("plg:backup:error", { id: backupId, type, error: error.message });
|
|
4680
6718
|
throw error;
|
|
4681
6719
|
} finally {
|
|
4682
6720
|
this.activeBackups.delete(backupId);
|
|
@@ -4897,7 +6935,7 @@ class BackupPlugin extends Plugin {
|
|
|
4897
6935
|
if (this.config.onRestoreStart) {
|
|
4898
6936
|
await this._executeHook(this.config.onRestoreStart, backupId, options);
|
|
4899
6937
|
}
|
|
4900
|
-
this.emit("
|
|
6938
|
+
this.emit("plg:backup:restore-start", { id: backupId, options });
|
|
4901
6939
|
const backup = await this.getBackupStatus(backupId);
|
|
4902
6940
|
if (!backup) {
|
|
4903
6941
|
throw new Error(`Backup '${backupId}' not found`);
|
|
@@ -4920,7 +6958,7 @@ class BackupPlugin extends Plugin {
|
|
|
4920
6958
|
if (this.config.onRestoreComplete) {
|
|
4921
6959
|
await this._executeHook(this.config.onRestoreComplete, backupId, { restored: restoredResources });
|
|
4922
6960
|
}
|
|
4923
|
-
this.emit("
|
|
6961
|
+
this.emit("plg:backup:restore-complete", {
|
|
4924
6962
|
id: backupId,
|
|
4925
6963
|
restored: restoredResources
|
|
4926
6964
|
});
|
|
@@ -4935,7 +6973,7 @@ class BackupPlugin extends Plugin {
|
|
|
4935
6973
|
if (this.config.onRestoreError) {
|
|
4936
6974
|
await this._executeHook(this.config.onRestoreError, backupId, { error });
|
|
4937
6975
|
}
|
|
4938
|
-
this.emit("
|
|
6976
|
+
this.emit("plg:backup:restore-error", { id: backupId, error: error.message });
|
|
4939
6977
|
throw error;
|
|
4940
6978
|
}
|
|
4941
6979
|
}
|
|
@@ -5185,7 +7223,7 @@ class BackupPlugin extends Plugin {
|
|
|
5185
7223
|
}
|
|
5186
7224
|
async stop() {
|
|
5187
7225
|
for (const backupId of this.activeBackups) {
|
|
5188
|
-
this.emit("
|
|
7226
|
+
this.emit("plg:backup:cancelled", { id: backupId });
|
|
5189
7227
|
}
|
|
5190
7228
|
this.activeBackups.clear();
|
|
5191
7229
|
if (this.driver) {
|
|
@@ -6709,7 +8747,7 @@ class CachePlugin extends Plugin {
|
|
|
6709
8747
|
const specificKey = await this.generateCacheKey(resource, method, { id: data.id });
|
|
6710
8748
|
const [ok2, err2] = await this.clearCacheWithRetry(resource.cache, specificKey);
|
|
6711
8749
|
if (!ok2) {
|
|
6712
|
-
this.emit("
|
|
8750
|
+
this.emit("plg:cache:clear-error", {
|
|
6713
8751
|
resource: resource.name,
|
|
6714
8752
|
method,
|
|
6715
8753
|
id: data.id,
|
|
@@ -6727,7 +8765,7 @@ class CachePlugin extends Plugin {
|
|
|
6727
8765
|
const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
|
|
6728
8766
|
const [ok2, err2] = await this.clearCacheWithRetry(resource.cache, partitionKeyPrefix);
|
|
6729
8767
|
if (!ok2) {
|
|
6730
|
-
this.emit("
|
|
8768
|
+
this.emit("plg:cache:clear-error", {
|
|
6731
8769
|
resource: resource.name,
|
|
6732
8770
|
partition: partitionName,
|
|
6733
8771
|
error: err2.message
|
|
@@ -6742,7 +8780,7 @@ class CachePlugin extends Plugin {
|
|
|
6742
8780
|
}
|
|
6743
8781
|
const [ok, err] = await this.clearCacheWithRetry(resource.cache, keyPrefix);
|
|
6744
8782
|
if (!ok) {
|
|
6745
|
-
this.emit("
|
|
8783
|
+
this.emit("plg:cache:clear-error", {
|
|
6746
8784
|
resource: resource.name,
|
|
6747
8785
|
type: "broad",
|
|
6748
8786
|
error: err.message
|
|
@@ -10642,7 +12680,7 @@ class GeoPlugin extends Plugin {
|
|
|
10642
12680
|
if (this.verbose) {
|
|
10643
12681
|
console.log(`[GeoPlugin] Installed with ${Object.keys(this.resources).length} resources`);
|
|
10644
12682
|
}
|
|
10645
|
-
this.emit("installed", {
|
|
12683
|
+
this.emit("db:plugin:installed", {
|
|
10646
12684
|
plugin: "GeoPlugin",
|
|
10647
12685
|
resources: Object.keys(this.resources)
|
|
10648
12686
|
});
|
|
@@ -11209,7 +13247,7 @@ class GeoPlugin extends Plugin {
|
|
|
11209
13247
|
if (this.verbose) {
|
|
11210
13248
|
console.log("[GeoPlugin] Uninstalled");
|
|
11211
13249
|
}
|
|
11212
|
-
this.emit("uninstalled", {
|
|
13250
|
+
this.emit("db:plugin:uninstalled", {
|
|
11213
13251
|
plugin: "GeoPlugin"
|
|
11214
13252
|
});
|
|
11215
13253
|
await super.uninstall();
|
|
@@ -13204,10 +15242,17 @@ class MLPlugin extends Plugin {
|
|
|
13204
15242
|
this.config = {
|
|
13205
15243
|
models: options.models || {},
|
|
13206
15244
|
verbose: options.verbose || false,
|
|
13207
|
-
minTrainingSamples: options.minTrainingSamples || 10
|
|
15245
|
+
minTrainingSamples: options.minTrainingSamples || 10,
|
|
15246
|
+
saveModel: options.saveModel !== false,
|
|
15247
|
+
// Default true
|
|
15248
|
+
saveTrainingData: options.saveTrainingData || false,
|
|
15249
|
+
enableVersioning: options.enableVersioning !== false
|
|
15250
|
+
// Default true
|
|
13208
15251
|
};
|
|
13209
|
-
requirePluginDependency("
|
|
15252
|
+
requirePluginDependency("ml-plugin");
|
|
13210
15253
|
this.models = {};
|
|
15254
|
+
this.modelVersions = /* @__PURE__ */ new Map();
|
|
15255
|
+
this.modelCache = /* @__PURE__ */ new Map();
|
|
13211
15256
|
this.training = /* @__PURE__ */ new Map();
|
|
13212
15257
|
this.insertCounters = /* @__PURE__ */ new Map();
|
|
13213
15258
|
this.intervals = [];
|
|
@@ -13231,6 +15276,8 @@ class MLPlugin extends Plugin {
|
|
|
13231
15276
|
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
13232
15277
|
await this._initializeModel(modelName, modelConfig);
|
|
13233
15278
|
}
|
|
15279
|
+
this._buildModelCache();
|
|
15280
|
+
this._injectResourceMethods();
|
|
13234
15281
|
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
13235
15282
|
if (modelConfig.autoTrain) {
|
|
13236
15283
|
this._setupAutoTraining(modelName, modelConfig);
|
|
@@ -13240,7 +15287,7 @@ class MLPlugin extends Plugin {
|
|
|
13240
15287
|
if (this.config.verbose) {
|
|
13241
15288
|
console.log(`[MLPlugin] Installed with ${Object.keys(this.models).length} models`);
|
|
13242
15289
|
}
|
|
13243
|
-
this.emit("installed", {
|
|
15290
|
+
this.emit("db:plugin:installed", {
|
|
13244
15291
|
plugin: "MLPlugin",
|
|
13245
15292
|
models: Object.keys(this.models)
|
|
13246
15293
|
});
|
|
@@ -13249,6 +15296,11 @@ class MLPlugin extends Plugin {
|
|
|
13249
15296
|
* Start the plugin
|
|
13250
15297
|
*/
|
|
13251
15298
|
async onStart() {
|
|
15299
|
+
if (this.config.enableVersioning) {
|
|
15300
|
+
for (const modelName of Object.keys(this.models)) {
|
|
15301
|
+
await this._initializeVersioning(modelName);
|
|
15302
|
+
}
|
|
15303
|
+
}
|
|
13252
15304
|
for (const modelName of Object.keys(this.models)) {
|
|
13253
15305
|
await this._loadModel(modelName);
|
|
13254
15306
|
}
|
|
@@ -13281,12 +15333,135 @@ class MLPlugin extends Plugin {
|
|
|
13281
15333
|
if (options.purgeData) {
|
|
13282
15334
|
for (const modelName of Object.keys(this.models)) {
|
|
13283
15335
|
await this._deleteModel(modelName);
|
|
15336
|
+
await this._deleteTrainingData(modelName);
|
|
15337
|
+
}
|
|
15338
|
+
if (this.config.verbose) {
|
|
15339
|
+
console.log("[MLPlugin] Purged all model data and training data");
|
|
13284
15340
|
}
|
|
15341
|
+
}
|
|
15342
|
+
}
|
|
15343
|
+
/**
|
|
15344
|
+
* Build model cache for fast lookup
|
|
15345
|
+
* @private
|
|
15346
|
+
*/
|
|
15347
|
+
_buildModelCache() {
|
|
15348
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
15349
|
+
const cacheKey = `${modelConfig.resource}_${modelConfig.target}`;
|
|
15350
|
+
this.modelCache.set(cacheKey, modelName);
|
|
13285
15351
|
if (this.config.verbose) {
|
|
13286
|
-
console.log(
|
|
15352
|
+
console.log(`[MLPlugin] Cached model "${modelName}" for ${modelConfig.resource}.predict(..., '${modelConfig.target}')`);
|
|
13287
15353
|
}
|
|
13288
15354
|
}
|
|
13289
15355
|
}
|
|
15356
|
+
/**
|
|
15357
|
+
* Inject ML methods into Resource instances
|
|
15358
|
+
* @private
|
|
15359
|
+
*/
|
|
15360
|
+
_injectResourceMethods() {
|
|
15361
|
+
if (!this.database._mlPlugin) {
|
|
15362
|
+
this.database._mlPlugin = this;
|
|
15363
|
+
}
|
|
15364
|
+
if (!this.database.Resource.prototype.predict) {
|
|
15365
|
+
this.database.Resource.prototype.predict = async function(input, targetAttribute) {
|
|
15366
|
+
const mlPlugin = this.database._mlPlugin;
|
|
15367
|
+
if (!mlPlugin) {
|
|
15368
|
+
throw new Error("MLPlugin not installed");
|
|
15369
|
+
}
|
|
15370
|
+
return await mlPlugin._resourcePredict(this.name, input, targetAttribute);
|
|
15371
|
+
};
|
|
15372
|
+
}
|
|
15373
|
+
if (!this.database.Resource.prototype.trainModel) {
|
|
15374
|
+
this.database.Resource.prototype.trainModel = async function(targetAttribute, options = {}) {
|
|
15375
|
+
const mlPlugin = this.database._mlPlugin;
|
|
15376
|
+
if (!mlPlugin) {
|
|
15377
|
+
throw new Error("MLPlugin not installed");
|
|
15378
|
+
}
|
|
15379
|
+
return await mlPlugin._resourceTrainModel(this.name, targetAttribute, options);
|
|
15380
|
+
};
|
|
15381
|
+
}
|
|
15382
|
+
if (!this.database.Resource.prototype.listModels) {
|
|
15383
|
+
this.database.Resource.prototype.listModels = function() {
|
|
15384
|
+
const mlPlugin = this.database._mlPlugin;
|
|
15385
|
+
if (!mlPlugin) {
|
|
15386
|
+
throw new Error("MLPlugin not installed");
|
|
15387
|
+
}
|
|
15388
|
+
return mlPlugin._resourceListModels(this.name);
|
|
15389
|
+
};
|
|
15390
|
+
}
|
|
15391
|
+
if (this.config.verbose) {
|
|
15392
|
+
console.log("[MLPlugin] Injected ML methods into Resource prototype");
|
|
15393
|
+
}
|
|
15394
|
+
}
|
|
15395
|
+
/**
|
|
15396
|
+
* Find model for a resource and target attribute
|
|
15397
|
+
* @private
|
|
15398
|
+
*/
|
|
15399
|
+
_findModelForResource(resourceName, targetAttribute) {
|
|
15400
|
+
const cacheKey = `${resourceName}_${targetAttribute}`;
|
|
15401
|
+
if (this.modelCache.has(cacheKey)) {
|
|
15402
|
+
return this.modelCache.get(cacheKey);
|
|
15403
|
+
}
|
|
15404
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
15405
|
+
if (modelConfig.resource === resourceName && modelConfig.target === targetAttribute) {
|
|
15406
|
+
this.modelCache.set(cacheKey, modelName);
|
|
15407
|
+
return modelName;
|
|
15408
|
+
}
|
|
15409
|
+
}
|
|
15410
|
+
return null;
|
|
15411
|
+
}
|
|
15412
|
+
/**
|
|
15413
|
+
* Resource predict implementation
|
|
15414
|
+
* @private
|
|
15415
|
+
*/
|
|
15416
|
+
async _resourcePredict(resourceName, input, targetAttribute) {
|
|
15417
|
+
const modelName = this._findModelForResource(resourceName, targetAttribute);
|
|
15418
|
+
if (!modelName) {
|
|
15419
|
+
throw new ModelNotFoundError(
|
|
15420
|
+
`No model found for resource "${resourceName}" with target "${targetAttribute}"`,
|
|
15421
|
+
{ resourceName, targetAttribute, availableModels: Object.keys(this.models) }
|
|
15422
|
+
);
|
|
15423
|
+
}
|
|
15424
|
+
if (this.config.verbose) {
|
|
15425
|
+
console.log(`[MLPlugin] Resource prediction: ${resourceName}.predict(..., '${targetAttribute}') -> model "${modelName}"`);
|
|
15426
|
+
}
|
|
15427
|
+
return await this.predict(modelName, input);
|
|
15428
|
+
}
|
|
15429
|
+
/**
|
|
15430
|
+
* Resource trainModel implementation
|
|
15431
|
+
* @private
|
|
15432
|
+
*/
|
|
15433
|
+
async _resourceTrainModel(resourceName, targetAttribute, options = {}) {
|
|
15434
|
+
const modelName = this._findModelForResource(resourceName, targetAttribute);
|
|
15435
|
+
if (!modelName) {
|
|
15436
|
+
throw new ModelNotFoundError(
|
|
15437
|
+
`No model found for resource "${resourceName}" with target "${targetAttribute}"`,
|
|
15438
|
+
{ resourceName, targetAttribute, availableModels: Object.keys(this.models) }
|
|
15439
|
+
);
|
|
15440
|
+
}
|
|
15441
|
+
if (this.config.verbose) {
|
|
15442
|
+
console.log(`[MLPlugin] Resource training: ${resourceName}.trainModel('${targetAttribute}') -> model "${modelName}"`);
|
|
15443
|
+
}
|
|
15444
|
+
return await this.train(modelName, options);
|
|
15445
|
+
}
|
|
15446
|
+
/**
|
|
15447
|
+
* List models for a resource
|
|
15448
|
+
* @private
|
|
15449
|
+
*/
|
|
15450
|
+
_resourceListModels(resourceName) {
|
|
15451
|
+
const models = [];
|
|
15452
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
15453
|
+
if (modelConfig.resource === resourceName) {
|
|
15454
|
+
models.push({
|
|
15455
|
+
name: modelName,
|
|
15456
|
+
type: modelConfig.type,
|
|
15457
|
+
target: modelConfig.target,
|
|
15458
|
+
features: modelConfig.features,
|
|
15459
|
+
isTrained: this.models[modelName]?.isTrained || false
|
|
15460
|
+
});
|
|
15461
|
+
}
|
|
15462
|
+
}
|
|
15463
|
+
return models;
|
|
15464
|
+
}
|
|
13290
15465
|
/**
|
|
13291
15466
|
* Validate model configuration
|
|
13292
15467
|
* @private
|
|
@@ -13437,12 +15612,47 @@ class MLPlugin extends Plugin {
|
|
|
13437
15612
|
if (this.config.verbose) {
|
|
13438
15613
|
console.log(`[MLPlugin] Fetching training data for "${modelName}"...`);
|
|
13439
15614
|
}
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
|
|
13443
|
-
|
|
13444
|
-
|
|
15615
|
+
let data;
|
|
15616
|
+
const partition = modelConfig.partition;
|
|
15617
|
+
if (partition && partition.name) {
|
|
15618
|
+
if (this.config.verbose) {
|
|
15619
|
+
console.log(`[MLPlugin] Using partition "${partition.name}" with values:`, partition.values);
|
|
15620
|
+
}
|
|
15621
|
+
const [ok, err, partitionData] = await tryFn(
|
|
15622
|
+
() => resource.listPartition(partition.name, partition.values)
|
|
13445
15623
|
);
|
|
15624
|
+
if (!ok) {
|
|
15625
|
+
throw new TrainingError(
|
|
15626
|
+
`Failed to fetch training data from partition: ${err.message}`,
|
|
15627
|
+
{ modelName, resource: modelConfig.resource, partition: partition.name, originalError: err.message }
|
|
15628
|
+
);
|
|
15629
|
+
}
|
|
15630
|
+
data = partitionData;
|
|
15631
|
+
} else {
|
|
15632
|
+
const [ok, err, allData] = await tryFn(() => resource.list());
|
|
15633
|
+
if (!ok) {
|
|
15634
|
+
throw new TrainingError(
|
|
15635
|
+
`Failed to fetch training data: ${err.message}`,
|
|
15636
|
+
{ modelName, resource: modelConfig.resource, originalError: err.message }
|
|
15637
|
+
);
|
|
15638
|
+
}
|
|
15639
|
+
data = allData;
|
|
15640
|
+
}
|
|
15641
|
+
if (modelConfig.filter && typeof modelConfig.filter === "function") {
|
|
15642
|
+
if (this.config.verbose) {
|
|
15643
|
+
console.log(`[MLPlugin] Applying custom filter function...`);
|
|
15644
|
+
}
|
|
15645
|
+
const originalLength = data.length;
|
|
15646
|
+
data = data.filter(modelConfig.filter);
|
|
15647
|
+
if (this.config.verbose) {
|
|
15648
|
+
console.log(`[MLPlugin] Filter reduced dataset from ${originalLength} to ${data.length} samples`);
|
|
15649
|
+
}
|
|
15650
|
+
}
|
|
15651
|
+
if (modelConfig.map && typeof modelConfig.map === "function") {
|
|
15652
|
+
if (this.config.verbose) {
|
|
15653
|
+
console.log(`[MLPlugin] Applying custom map function...`);
|
|
15654
|
+
}
|
|
15655
|
+
data = data.map(modelConfig.map);
|
|
13446
15656
|
}
|
|
13447
15657
|
if (!data || data.length < this.config.minTrainingSamples) {
|
|
13448
15658
|
throw new TrainingError(
|
|
@@ -13453,13 +15663,20 @@ class MLPlugin extends Plugin {
|
|
|
13453
15663
|
if (this.config.verbose) {
|
|
13454
15664
|
console.log(`[MLPlugin] Training "${modelName}" with ${data.length} samples...`);
|
|
13455
15665
|
}
|
|
15666
|
+
const shouldSaveTrainingData = modelConfig.saveTrainingData !== void 0 ? modelConfig.saveTrainingData : this.config.saveTrainingData;
|
|
15667
|
+
if (shouldSaveTrainingData) {
|
|
15668
|
+
await this._saveTrainingData(modelName, data);
|
|
15669
|
+
}
|
|
13456
15670
|
const result = await model.train(data);
|
|
13457
|
-
|
|
15671
|
+
const shouldSaveModel = modelConfig.saveModel !== void 0 ? modelConfig.saveModel : this.config.saveModel;
|
|
15672
|
+
if (shouldSaveModel) {
|
|
15673
|
+
await this._saveModel(modelName);
|
|
15674
|
+
}
|
|
13458
15675
|
this.stats.totalTrainings++;
|
|
13459
15676
|
if (this.config.verbose) {
|
|
13460
15677
|
console.log(`[MLPlugin] Training completed for "${modelName}":`, result);
|
|
13461
15678
|
}
|
|
13462
|
-
this.emit("
|
|
15679
|
+
this.emit("plg:ml:model-trained", {
|
|
13463
15680
|
modelName,
|
|
13464
15681
|
type: modelConfig.type,
|
|
13465
15682
|
result
|
|
@@ -13495,7 +15712,7 @@ class MLPlugin extends Plugin {
|
|
|
13495
15712
|
try {
|
|
13496
15713
|
const result = await model.predict(input);
|
|
13497
15714
|
this.stats.totalPredictions++;
|
|
13498
|
-
this.emit("prediction", {
|
|
15715
|
+
this.emit("plg:ml:prediction", {
|
|
13499
15716
|
modelName,
|
|
13500
15717
|
input,
|
|
13501
15718
|
result
|
|
@@ -13603,6 +15820,79 @@ class MLPlugin extends Plugin {
|
|
|
13603
15820
|
console.log(`[MLPlugin] Imported model "${modelName}"`);
|
|
13604
15821
|
}
|
|
13605
15822
|
}
|
|
15823
|
+
/**
|
|
15824
|
+
* Initialize versioning for a model
|
|
15825
|
+
* @private
|
|
15826
|
+
*/
|
|
15827
|
+
async _initializeVersioning(modelName) {
|
|
15828
|
+
try {
|
|
15829
|
+
const storage = this.getStorage();
|
|
15830
|
+
const modelConfig = this.config.models[modelName];
|
|
15831
|
+
const resourceName = modelConfig.resource;
|
|
15832
|
+
const [ok, err, versionInfo] = await tryFn(
|
|
15833
|
+
() => storage.get(storage.getPluginKey(resourceName, "metadata", modelName, "versions"))
|
|
15834
|
+
);
|
|
15835
|
+
if (ok && versionInfo) {
|
|
15836
|
+
this.modelVersions.set(modelName, {
|
|
15837
|
+
currentVersion: versionInfo.currentVersion || 1,
|
|
15838
|
+
latestVersion: versionInfo.latestVersion || 1
|
|
15839
|
+
});
|
|
15840
|
+
if (this.config.verbose) {
|
|
15841
|
+
console.log(`[MLPlugin] Loaded version info for "${modelName}": v${versionInfo.currentVersion}`);
|
|
15842
|
+
}
|
|
15843
|
+
} else {
|
|
15844
|
+
this.modelVersions.set(modelName, {
|
|
15845
|
+
currentVersion: 1,
|
|
15846
|
+
latestVersion: 0
|
|
15847
|
+
// No versions yet
|
|
15848
|
+
});
|
|
15849
|
+
if (this.config.verbose) {
|
|
15850
|
+
console.log(`[MLPlugin] Initialized versioning for "${modelName}"`);
|
|
15851
|
+
}
|
|
15852
|
+
}
|
|
15853
|
+
} catch (error) {
|
|
15854
|
+
console.error(`[MLPlugin] Failed to initialize versioning for "${modelName}":`, error.message);
|
|
15855
|
+
this.modelVersions.set(modelName, { currentVersion: 1, latestVersion: 0 });
|
|
15856
|
+
}
|
|
15857
|
+
}
|
|
15858
|
+
/**
|
|
15859
|
+
* Get next version number for a model
|
|
15860
|
+
* @private
|
|
15861
|
+
*/
|
|
15862
|
+
_getNextVersion(modelName) {
|
|
15863
|
+
const versionInfo = this.modelVersions.get(modelName) || { latestVersion: 0 };
|
|
15864
|
+
return versionInfo.latestVersion + 1;
|
|
15865
|
+
}
|
|
15866
|
+
/**
|
|
15867
|
+
* Update version info in storage
|
|
15868
|
+
* @private
|
|
15869
|
+
*/
|
|
15870
|
+
async _updateVersionInfo(modelName, version) {
|
|
15871
|
+
try {
|
|
15872
|
+
const storage = this.getStorage();
|
|
15873
|
+
const modelConfig = this.config.models[modelName];
|
|
15874
|
+
const resourceName = modelConfig.resource;
|
|
15875
|
+
const versionInfo = this.modelVersions.get(modelName) || { currentVersion: 1, latestVersion: 0 };
|
|
15876
|
+
versionInfo.latestVersion = Math.max(versionInfo.latestVersion, version);
|
|
15877
|
+
versionInfo.currentVersion = version;
|
|
15878
|
+
this.modelVersions.set(modelName, versionInfo);
|
|
15879
|
+
await storage.set(
|
|
15880
|
+
storage.getPluginKey(resourceName, "metadata", modelName, "versions"),
|
|
15881
|
+
{
|
|
15882
|
+
modelName,
|
|
15883
|
+
currentVersion: versionInfo.currentVersion,
|
|
15884
|
+
latestVersion: versionInfo.latestVersion,
|
|
15885
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15886
|
+
},
|
|
15887
|
+
{ behavior: "body-overflow" }
|
|
15888
|
+
);
|
|
15889
|
+
if (this.config.verbose) {
|
|
15890
|
+
console.log(`[MLPlugin] Updated version info for "${modelName}": current=v${versionInfo.currentVersion}, latest=v${versionInfo.latestVersion}`);
|
|
15891
|
+
}
|
|
15892
|
+
} catch (error) {
|
|
15893
|
+
console.error(`[MLPlugin] Failed to update version info for "${modelName}":`, error.message);
|
|
15894
|
+
}
|
|
15895
|
+
}
|
|
13606
15896
|
/**
|
|
13607
15897
|
* Save model to plugin storage
|
|
13608
15898
|
* @private
|
|
@@ -13610,6 +15900,8 @@ class MLPlugin extends Plugin {
|
|
|
13610
15900
|
async _saveModel(modelName) {
|
|
13611
15901
|
try {
|
|
13612
15902
|
const storage = this.getStorage();
|
|
15903
|
+
const modelConfig = this.config.models[modelName];
|
|
15904
|
+
const resourceName = modelConfig.resource;
|
|
13613
15905
|
const exportedModel = await this.models[modelName].export();
|
|
13614
15906
|
if (!exportedModel) {
|
|
13615
15907
|
if (this.config.verbose) {
|
|
@@ -13617,18 +15909,177 @@ class MLPlugin extends Plugin {
|
|
|
13617
15909
|
}
|
|
13618
15910
|
return;
|
|
13619
15911
|
}
|
|
13620
|
-
|
|
13621
|
-
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
15912
|
+
const enableVersioning = this.config.enableVersioning;
|
|
15913
|
+
if (enableVersioning) {
|
|
15914
|
+
const version = this._getNextVersion(modelName);
|
|
15915
|
+
const modelStats = this.models[modelName].getStats();
|
|
15916
|
+
await storage.set(
|
|
15917
|
+
storage.getPluginKey(resourceName, "models", modelName, `v${version}`),
|
|
15918
|
+
{
|
|
15919
|
+
modelName,
|
|
15920
|
+
version,
|
|
15921
|
+
type: "model",
|
|
15922
|
+
modelData: exportedModel,
|
|
15923
|
+
// TensorFlow.js model object (will go to body)
|
|
15924
|
+
metrics: {
|
|
15925
|
+
loss: modelStats.loss,
|
|
15926
|
+
accuracy: modelStats.accuracy,
|
|
15927
|
+
samples: modelStats.samples
|
|
15928
|
+
},
|
|
15929
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15930
|
+
},
|
|
15931
|
+
{ behavior: "body-only" }
|
|
15932
|
+
// Large binary data goes to S3 body
|
|
15933
|
+
);
|
|
15934
|
+
await this._updateVersionInfo(modelName, version);
|
|
15935
|
+
await storage.set(
|
|
15936
|
+
storage.getPluginKey(resourceName, "metadata", modelName, "active"),
|
|
15937
|
+
{
|
|
15938
|
+
modelName,
|
|
15939
|
+
version,
|
|
15940
|
+
type: "reference",
|
|
15941
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15942
|
+
},
|
|
15943
|
+
{ behavior: "body-overflow" }
|
|
15944
|
+
// Small metadata
|
|
15945
|
+
);
|
|
15946
|
+
if (this.config.verbose) {
|
|
15947
|
+
console.log(`[MLPlugin] Saved model "${modelName}" v${version} to S3 (resource=${resourceName}/plugin=ml/models/${modelName}/v${version})`);
|
|
15948
|
+
}
|
|
15949
|
+
} else {
|
|
15950
|
+
await storage.set(
|
|
15951
|
+
storage.getPluginKey(resourceName, "models", modelName, "latest"),
|
|
15952
|
+
{
|
|
15953
|
+
modelName,
|
|
15954
|
+
type: "model",
|
|
15955
|
+
modelData: exportedModel,
|
|
15956
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15957
|
+
},
|
|
15958
|
+
{ behavior: "body-only" }
|
|
15959
|
+
);
|
|
15960
|
+
if (this.config.verbose) {
|
|
15961
|
+
console.log(`[MLPlugin] Saved model "${modelName}" to S3 (resource=${resourceName}/plugin=ml/models/${modelName}/latest)`);
|
|
15962
|
+
}
|
|
13627
15963
|
}
|
|
13628
15964
|
} catch (error) {
|
|
13629
15965
|
console.error(`[MLPlugin] Failed to save model "${modelName}":`, error.message);
|
|
13630
15966
|
}
|
|
13631
15967
|
}
|
|
15968
|
+
/**
|
|
15969
|
+
* Save intermediate training data to plugin storage (incremental - only new samples)
|
|
15970
|
+
* @private
|
|
15971
|
+
*/
|
|
15972
|
+
async _saveTrainingData(modelName, rawData) {
|
|
15973
|
+
try {
|
|
15974
|
+
const storage = this.getStorage();
|
|
15975
|
+
const model = this.models[modelName];
|
|
15976
|
+
const modelConfig = this.config.models[modelName];
|
|
15977
|
+
const resourceName = modelConfig.resource;
|
|
15978
|
+
const modelStats = model.getStats();
|
|
15979
|
+
const enableVersioning = this.config.enableVersioning;
|
|
15980
|
+
const processedData = rawData.map((item) => {
|
|
15981
|
+
const features = {};
|
|
15982
|
+
modelConfig.features.forEach((feature) => {
|
|
15983
|
+
features[feature] = item[feature];
|
|
15984
|
+
});
|
|
15985
|
+
return {
|
|
15986
|
+
id: item.id || `${Date.now()}_${Math.random()}`,
|
|
15987
|
+
// Use record ID or generate
|
|
15988
|
+
features,
|
|
15989
|
+
target: item[modelConfig.target]
|
|
15990
|
+
};
|
|
15991
|
+
});
|
|
15992
|
+
if (enableVersioning) {
|
|
15993
|
+
const version = this._getNextVersion(modelName);
|
|
15994
|
+
const [ok, err, existing] = await tryFn(
|
|
15995
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "history", modelName))
|
|
15996
|
+
);
|
|
15997
|
+
let history = [];
|
|
15998
|
+
let previousSampleIds = /* @__PURE__ */ new Set();
|
|
15999
|
+
if (ok && existing && existing.history) {
|
|
16000
|
+
history = existing.history;
|
|
16001
|
+
history.forEach((entry) => {
|
|
16002
|
+
if (entry.sampleIds) {
|
|
16003
|
+
entry.sampleIds.forEach((id) => previousSampleIds.add(id));
|
|
16004
|
+
}
|
|
16005
|
+
});
|
|
16006
|
+
}
|
|
16007
|
+
const currentSampleIds = new Set(processedData.map((d) => d.id));
|
|
16008
|
+
const newSamples = processedData.filter((d) => !previousSampleIds.has(d.id));
|
|
16009
|
+
const newSampleIds = newSamples.map((d) => d.id);
|
|
16010
|
+
if (newSamples.length > 0) {
|
|
16011
|
+
await storage.set(
|
|
16012
|
+
storage.getPluginKey(resourceName, "training", "data", modelName, `v${version}`),
|
|
16013
|
+
{
|
|
16014
|
+
modelName,
|
|
16015
|
+
version,
|
|
16016
|
+
samples: newSamples,
|
|
16017
|
+
// Only new samples
|
|
16018
|
+
features: modelConfig.features,
|
|
16019
|
+
target: modelConfig.target,
|
|
16020
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16021
|
+
},
|
|
16022
|
+
{ behavior: "body-only" }
|
|
16023
|
+
// Dataset goes to S3 body
|
|
16024
|
+
);
|
|
16025
|
+
}
|
|
16026
|
+
const historyEntry = {
|
|
16027
|
+
version,
|
|
16028
|
+
totalSamples: processedData.length,
|
|
16029
|
+
// Total cumulative
|
|
16030
|
+
newSamples: newSamples.length,
|
|
16031
|
+
// Only new in this version
|
|
16032
|
+
sampleIds: Array.from(currentSampleIds),
|
|
16033
|
+
// All IDs for this version
|
|
16034
|
+
newSampleIds,
|
|
16035
|
+
// IDs of new samples
|
|
16036
|
+
storageKey: newSamples.length > 0 ? `training/data/${modelName}/v${version}` : null,
|
|
16037
|
+
metrics: {
|
|
16038
|
+
loss: modelStats.loss,
|
|
16039
|
+
accuracy: modelStats.accuracy,
|
|
16040
|
+
r2: modelStats.r2
|
|
16041
|
+
},
|
|
16042
|
+
trainedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16043
|
+
};
|
|
16044
|
+
history.push(historyEntry);
|
|
16045
|
+
await storage.set(
|
|
16046
|
+
storage.getPluginKey(resourceName, "training", "history", modelName),
|
|
16047
|
+
{
|
|
16048
|
+
modelName,
|
|
16049
|
+
type: "training_history",
|
|
16050
|
+
totalTrainings: history.length,
|
|
16051
|
+
latestVersion: version,
|
|
16052
|
+
history,
|
|
16053
|
+
// Array of metadata entries (not full data)
|
|
16054
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16055
|
+
},
|
|
16056
|
+
{ behavior: "body-overflow" }
|
|
16057
|
+
// History metadata
|
|
16058
|
+
);
|
|
16059
|
+
if (this.config.verbose) {
|
|
16060
|
+
console.log(`[MLPlugin] Saved training data for "${modelName}" v${version}: ${newSamples.length} new samples (total: ${processedData.length}, storage: resource=${resourceName}/plugin=ml/training/data/${modelName}/v${version})`);
|
|
16061
|
+
}
|
|
16062
|
+
} else {
|
|
16063
|
+
await storage.set(
|
|
16064
|
+
storage.getPluginKey(resourceName, "training", "data", modelName, "latest"),
|
|
16065
|
+
{
|
|
16066
|
+
modelName,
|
|
16067
|
+
type: "training_data",
|
|
16068
|
+
samples: processedData,
|
|
16069
|
+
features: modelConfig.features,
|
|
16070
|
+
target: modelConfig.target,
|
|
16071
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16072
|
+
},
|
|
16073
|
+
{ behavior: "body-only" }
|
|
16074
|
+
);
|
|
16075
|
+
if (this.config.verbose) {
|
|
16076
|
+
console.log(`[MLPlugin] Saved training data for "${modelName}" (${processedData.length} samples) to S3 (resource=${resourceName}/plugin=ml/training/data/${modelName}/latest)`);
|
|
16077
|
+
}
|
|
16078
|
+
}
|
|
16079
|
+
} catch (error) {
|
|
16080
|
+
console.error(`[MLPlugin] Failed to save training data for "${modelName}":`, error.message);
|
|
16081
|
+
}
|
|
16082
|
+
}
|
|
13632
16083
|
/**
|
|
13633
16084
|
* Load model from plugin storage
|
|
13634
16085
|
* @private
|
|
@@ -13636,32 +16087,155 @@ class MLPlugin extends Plugin {
|
|
|
13636
16087
|
async _loadModel(modelName) {
|
|
13637
16088
|
try {
|
|
13638
16089
|
const storage = this.getStorage();
|
|
13639
|
-
const
|
|
13640
|
-
|
|
16090
|
+
const modelConfig = this.config.models[modelName];
|
|
16091
|
+
const resourceName = modelConfig.resource;
|
|
16092
|
+
const enableVersioning = this.config.enableVersioning;
|
|
16093
|
+
if (enableVersioning) {
|
|
16094
|
+
const [okRef, errRef, activeRef] = await tryFn(
|
|
16095
|
+
() => storage.get(storage.getPluginKey(resourceName, "metadata", modelName, "active"))
|
|
16096
|
+
);
|
|
16097
|
+
if (okRef && activeRef && activeRef.version) {
|
|
16098
|
+
const version = activeRef.version;
|
|
16099
|
+
const [ok, err, versionData] = await tryFn(
|
|
16100
|
+
() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version}`))
|
|
16101
|
+
);
|
|
16102
|
+
if (ok && versionData && versionData.modelData) {
|
|
16103
|
+
await this.models[modelName].import(versionData.modelData);
|
|
16104
|
+
if (this.config.verbose) {
|
|
16105
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (active) from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/v${version})`);
|
|
16106
|
+
}
|
|
16107
|
+
return;
|
|
16108
|
+
}
|
|
16109
|
+
}
|
|
16110
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
16111
|
+
if (versionInfo && versionInfo.latestVersion > 0) {
|
|
16112
|
+
const version = versionInfo.latestVersion;
|
|
16113
|
+
const [ok, err, versionData] = await tryFn(
|
|
16114
|
+
() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version}`))
|
|
16115
|
+
);
|
|
16116
|
+
if (ok && versionData && versionData.modelData) {
|
|
16117
|
+
await this.models[modelName].import(versionData.modelData);
|
|
16118
|
+
if (this.config.verbose) {
|
|
16119
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (latest) from S3`);
|
|
16120
|
+
}
|
|
16121
|
+
return;
|
|
16122
|
+
}
|
|
16123
|
+
}
|
|
13641
16124
|
if (this.config.verbose) {
|
|
13642
|
-
console.log(`[MLPlugin] No saved model found for "${modelName}"`);
|
|
16125
|
+
console.log(`[MLPlugin] No saved model versions found for "${modelName}"`);
|
|
16126
|
+
}
|
|
16127
|
+
} else {
|
|
16128
|
+
const [ok, err, record] = await tryFn(
|
|
16129
|
+
() => storage.get(storage.getPluginKey(resourceName, "models", modelName, "latest"))
|
|
16130
|
+
);
|
|
16131
|
+
if (!ok || !record || !record.modelData) {
|
|
16132
|
+
if (this.config.verbose) {
|
|
16133
|
+
console.log(`[MLPlugin] No saved model found for "${modelName}"`);
|
|
16134
|
+
}
|
|
16135
|
+
return;
|
|
16136
|
+
}
|
|
16137
|
+
await this.models[modelName].import(record.modelData);
|
|
16138
|
+
if (this.config.verbose) {
|
|
16139
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/latest)`);
|
|
13643
16140
|
}
|
|
13644
|
-
return;
|
|
13645
|
-
}
|
|
13646
|
-
const modelData = JSON.parse(record.data);
|
|
13647
|
-
await this.models[modelName].import(modelData);
|
|
13648
|
-
if (this.config.verbose) {
|
|
13649
|
-
console.log(`[MLPlugin] Loaded model "${modelName}" from plugin storage`);
|
|
13650
16141
|
}
|
|
13651
16142
|
} catch (error) {
|
|
13652
16143
|
console.error(`[MLPlugin] Failed to load model "${modelName}":`, error.message);
|
|
13653
16144
|
}
|
|
13654
16145
|
}
|
|
13655
16146
|
/**
|
|
13656
|
-
*
|
|
16147
|
+
* Load training data from plugin storage (reconstructs specific version from incremental data)
|
|
16148
|
+
* @param {string} modelName - Model name
|
|
16149
|
+
* @param {number} version - Version number (optional, defaults to latest)
|
|
16150
|
+
* @returns {Object|null} Training data or null if not found
|
|
16151
|
+
*/
|
|
16152
|
+
async getTrainingData(modelName, version = null) {
|
|
16153
|
+
try {
|
|
16154
|
+
const storage = this.getStorage();
|
|
16155
|
+
const modelConfig = this.config.models[modelName];
|
|
16156
|
+
const resourceName = modelConfig.resource;
|
|
16157
|
+
const enableVersioning = this.config.enableVersioning;
|
|
16158
|
+
if (!enableVersioning) {
|
|
16159
|
+
const [ok, err, record] = await tryFn(
|
|
16160
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "data", modelName, "latest"))
|
|
16161
|
+
);
|
|
16162
|
+
if (!ok || !record) {
|
|
16163
|
+
if (this.config.verbose) {
|
|
16164
|
+
console.log(`[MLPlugin] No saved training data found for "${modelName}"`);
|
|
16165
|
+
}
|
|
16166
|
+
return null;
|
|
16167
|
+
}
|
|
16168
|
+
return {
|
|
16169
|
+
modelName: record.modelName,
|
|
16170
|
+
samples: record.samples,
|
|
16171
|
+
features: record.features,
|
|
16172
|
+
target: record.target,
|
|
16173
|
+
data: record.samples,
|
|
16174
|
+
savedAt: record.savedAt
|
|
16175
|
+
};
|
|
16176
|
+
}
|
|
16177
|
+
const [okHistory, errHistory, historyData] = await tryFn(
|
|
16178
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "history", modelName))
|
|
16179
|
+
);
|
|
16180
|
+
if (!okHistory || !historyData || !historyData.history) {
|
|
16181
|
+
if (this.config.verbose) {
|
|
16182
|
+
console.log(`[MLPlugin] No training history found for "${modelName}"`);
|
|
16183
|
+
}
|
|
16184
|
+
return null;
|
|
16185
|
+
}
|
|
16186
|
+
const targetVersion = version || historyData.latestVersion;
|
|
16187
|
+
const reconstructedSamples = [];
|
|
16188
|
+
for (const entry of historyData.history) {
|
|
16189
|
+
if (entry.version > targetVersion) break;
|
|
16190
|
+
if (entry.storageKey && entry.newSamples > 0) {
|
|
16191
|
+
const [ok, err, versionData] = await tryFn(
|
|
16192
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "data", modelName, `v${entry.version}`))
|
|
16193
|
+
);
|
|
16194
|
+
if (ok && versionData && versionData.samples) {
|
|
16195
|
+
reconstructedSamples.push(...versionData.samples);
|
|
16196
|
+
}
|
|
16197
|
+
}
|
|
16198
|
+
}
|
|
16199
|
+
const targetEntry = historyData.history.find((e) => e.version === targetVersion);
|
|
16200
|
+
return {
|
|
16201
|
+
modelName,
|
|
16202
|
+
version: targetVersion,
|
|
16203
|
+
samples: reconstructedSamples,
|
|
16204
|
+
totalSamples: reconstructedSamples.length,
|
|
16205
|
+
features: modelConfig.features,
|
|
16206
|
+
target: modelConfig.target,
|
|
16207
|
+
metrics: targetEntry?.metrics,
|
|
16208
|
+
savedAt: targetEntry?.trainedAt
|
|
16209
|
+
};
|
|
16210
|
+
} catch (error) {
|
|
16211
|
+
console.error(`[MLPlugin] Failed to load training data for "${modelName}":`, error.message);
|
|
16212
|
+
return null;
|
|
16213
|
+
}
|
|
16214
|
+
}
|
|
16215
|
+
/**
|
|
16216
|
+
* Delete model from plugin storage (all versions)
|
|
13657
16217
|
* @private
|
|
13658
16218
|
*/
|
|
13659
16219
|
async _deleteModel(modelName) {
|
|
13660
16220
|
try {
|
|
13661
16221
|
const storage = this.getStorage();
|
|
13662
|
-
|
|
16222
|
+
const modelConfig = this.config.models[modelName];
|
|
16223
|
+
const resourceName = modelConfig.resource;
|
|
16224
|
+
const enableVersioning = this.config.enableVersioning;
|
|
16225
|
+
if (enableVersioning) {
|
|
16226
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
16227
|
+
if (versionInfo && versionInfo.latestVersion > 0) {
|
|
16228
|
+
for (let v = 1; v <= versionInfo.latestVersion; v++) {
|
|
16229
|
+
await storage.delete(storage.getPluginKey(resourceName, "models", modelName, `v${v}`));
|
|
16230
|
+
}
|
|
16231
|
+
}
|
|
16232
|
+
await storage.delete(storage.getPluginKey(resourceName, "metadata", modelName, "active"));
|
|
16233
|
+
await storage.delete(storage.getPluginKey(resourceName, "metadata", modelName, "versions"));
|
|
16234
|
+
} else {
|
|
16235
|
+
await storage.delete(storage.getPluginKey(resourceName, "models", modelName, "latest"));
|
|
16236
|
+
}
|
|
13663
16237
|
if (this.config.verbose) {
|
|
13664
|
-
console.log(`[MLPlugin] Deleted model "${modelName}" from plugin
|
|
16238
|
+
console.log(`[MLPlugin] Deleted model "${modelName}" from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/)`);
|
|
13665
16239
|
}
|
|
13666
16240
|
} catch (error) {
|
|
13667
16241
|
if (this.config.verbose) {
|
|
@@ -13669,6 +16243,247 @@ class MLPlugin extends Plugin {
|
|
|
13669
16243
|
}
|
|
13670
16244
|
}
|
|
13671
16245
|
}
|
|
16246
|
+
/**
|
|
16247
|
+
* Delete training data from plugin storage (all versions)
|
|
16248
|
+
* @private
|
|
16249
|
+
*/
|
|
16250
|
+
async _deleteTrainingData(modelName) {
|
|
16251
|
+
try {
|
|
16252
|
+
const storage = this.getStorage();
|
|
16253
|
+
const modelConfig = this.config.models[modelName];
|
|
16254
|
+
const resourceName = modelConfig.resource;
|
|
16255
|
+
const enableVersioning = this.config.enableVersioning;
|
|
16256
|
+
if (enableVersioning) {
|
|
16257
|
+
const [ok, err, historyData] = await tryFn(
|
|
16258
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "history", modelName))
|
|
16259
|
+
);
|
|
16260
|
+
if (ok && historyData && historyData.history) {
|
|
16261
|
+
for (const entry of historyData.history) {
|
|
16262
|
+
if (entry.storageKey) {
|
|
16263
|
+
await storage.delete(storage.getPluginKey(resourceName, "training", "data", modelName, `v${entry.version}`));
|
|
16264
|
+
}
|
|
16265
|
+
}
|
|
16266
|
+
}
|
|
16267
|
+
await storage.delete(storage.getPluginKey(resourceName, "training", "history", modelName));
|
|
16268
|
+
} else {
|
|
16269
|
+
await storage.delete(storage.getPluginKey(resourceName, "training", "data", modelName, "latest"));
|
|
16270
|
+
}
|
|
16271
|
+
if (this.config.verbose) {
|
|
16272
|
+
console.log(`[MLPlugin] Deleted training data for "${modelName}" from S3 (resource=${resourceName}/plugin=ml/training/)`);
|
|
16273
|
+
}
|
|
16274
|
+
} catch (error) {
|
|
16275
|
+
if (this.config.verbose) {
|
|
16276
|
+
console.log(`[MLPlugin] Could not delete training data "${modelName}": ${error.message}`);
|
|
16277
|
+
}
|
|
16278
|
+
}
|
|
16279
|
+
}
|
|
16280
|
+
/**
|
|
16281
|
+
* List all versions of a model
|
|
16282
|
+
* @param {string} modelName - Model name
|
|
16283
|
+
* @returns {Array} List of version info
|
|
16284
|
+
*/
|
|
16285
|
+
async listModelVersions(modelName) {
|
|
16286
|
+
if (!this.config.enableVersioning) {
|
|
16287
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16288
|
+
}
|
|
16289
|
+
try {
|
|
16290
|
+
const storage = this.getStorage();
|
|
16291
|
+
const modelConfig = this.config.models[modelName];
|
|
16292
|
+
const resourceName = modelConfig.resource;
|
|
16293
|
+
const versionInfo = this.modelVersions.get(modelName) || { latestVersion: 0 };
|
|
16294
|
+
const versions = [];
|
|
16295
|
+
for (let v = 1; v <= versionInfo.latestVersion; v++) {
|
|
16296
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${v}`)));
|
|
16297
|
+
if (ok && versionData) {
|
|
16298
|
+
versions.push({
|
|
16299
|
+
version: v,
|
|
16300
|
+
savedAt: versionData.savedAt,
|
|
16301
|
+
isCurrent: v === versionInfo.currentVersion,
|
|
16302
|
+
metrics: versionData.metrics
|
|
16303
|
+
});
|
|
16304
|
+
}
|
|
16305
|
+
}
|
|
16306
|
+
return versions;
|
|
16307
|
+
} catch (error) {
|
|
16308
|
+
console.error(`[MLPlugin] Failed to list versions for "${modelName}":`, error.message);
|
|
16309
|
+
return [];
|
|
16310
|
+
}
|
|
16311
|
+
}
|
|
16312
|
+
/**
|
|
16313
|
+
* Load a specific version of a model
|
|
16314
|
+
* @param {string} modelName - Model name
|
|
16315
|
+
* @param {number} version - Version number
|
|
16316
|
+
*/
|
|
16317
|
+
async loadModelVersion(modelName, version) {
|
|
16318
|
+
if (!this.config.enableVersioning) {
|
|
16319
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16320
|
+
}
|
|
16321
|
+
if (!this.models[modelName]) {
|
|
16322
|
+
throw new ModelNotFoundError(`Model "${modelName}" not found`, { modelName });
|
|
16323
|
+
}
|
|
16324
|
+
try {
|
|
16325
|
+
const storage = this.getStorage();
|
|
16326
|
+
const modelConfig = this.config.models[modelName];
|
|
16327
|
+
const resourceName = modelConfig.resource;
|
|
16328
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version}`)));
|
|
16329
|
+
if (!ok || !versionData) {
|
|
16330
|
+
throw new MLError(`Version ${version} not found for model "${modelName}"`, { modelName, version });
|
|
16331
|
+
}
|
|
16332
|
+
if (!versionData.modelData) {
|
|
16333
|
+
throw new MLError(`Model data not found in version ${version}`, { modelName, version });
|
|
16334
|
+
}
|
|
16335
|
+
await this.models[modelName].import(versionData.modelData);
|
|
16336
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
16337
|
+
if (versionInfo) {
|
|
16338
|
+
versionInfo.currentVersion = version;
|
|
16339
|
+
this.modelVersions.set(modelName, versionInfo);
|
|
16340
|
+
}
|
|
16341
|
+
if (this.config.verbose) {
|
|
16342
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version}`);
|
|
16343
|
+
}
|
|
16344
|
+
return {
|
|
16345
|
+
version,
|
|
16346
|
+
metrics: versionData.metrics ? JSON.parse(versionData.metrics) : {},
|
|
16347
|
+
savedAt: versionData.savedAt
|
|
16348
|
+
};
|
|
16349
|
+
} catch (error) {
|
|
16350
|
+
console.error(`[MLPlugin] Failed to load version ${version} for "${modelName}":`, error.message);
|
|
16351
|
+
throw error;
|
|
16352
|
+
}
|
|
16353
|
+
}
|
|
16354
|
+
/**
|
|
16355
|
+
* Set active version for a model (used for predictions)
|
|
16356
|
+
* @param {string} modelName - Model name
|
|
16357
|
+
* @param {number} version - Version number
|
|
16358
|
+
*/
|
|
16359
|
+
async setActiveVersion(modelName, version) {
|
|
16360
|
+
if (!this.config.enableVersioning) {
|
|
16361
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16362
|
+
}
|
|
16363
|
+
const modelConfig = this.config.models[modelName];
|
|
16364
|
+
const resourceName = modelConfig.resource;
|
|
16365
|
+
await this.loadModelVersion(modelName, version);
|
|
16366
|
+
await this._updateVersionInfo(modelName, version);
|
|
16367
|
+
const storage = this.getStorage();
|
|
16368
|
+
await storage.set(storage.getPluginKey(resourceName, "metadata", modelName, "active"), {
|
|
16369
|
+
modelName,
|
|
16370
|
+
version,
|
|
16371
|
+
type: "reference",
|
|
16372
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16373
|
+
});
|
|
16374
|
+
if (this.config.verbose) {
|
|
16375
|
+
console.log(`[MLPlugin] Set model "${modelName}" active version to v${version}`);
|
|
16376
|
+
}
|
|
16377
|
+
return { modelName, version };
|
|
16378
|
+
}
|
|
16379
|
+
/**
|
|
16380
|
+
* Get training history for a model
|
|
16381
|
+
* @param {string} modelName - Model name
|
|
16382
|
+
* @returns {Array} Training history
|
|
16383
|
+
*/
|
|
16384
|
+
async getTrainingHistory(modelName) {
|
|
16385
|
+
if (!this.config.enableVersioning) {
|
|
16386
|
+
return await this.getTrainingData(modelName);
|
|
16387
|
+
}
|
|
16388
|
+
try {
|
|
16389
|
+
const storage = this.getStorage();
|
|
16390
|
+
const modelConfig = this.config.models[modelName];
|
|
16391
|
+
const resourceName = modelConfig.resource;
|
|
16392
|
+
const [ok, err, historyData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "training", "history", modelName)));
|
|
16393
|
+
if (!ok || !historyData) {
|
|
16394
|
+
return null;
|
|
16395
|
+
}
|
|
16396
|
+
return {
|
|
16397
|
+
modelName: historyData.modelName,
|
|
16398
|
+
totalTrainings: historyData.totalTrainings,
|
|
16399
|
+
latestVersion: historyData.latestVersion,
|
|
16400
|
+
history: JSON.parse(historyData.history),
|
|
16401
|
+
updatedAt: historyData.updatedAt
|
|
16402
|
+
};
|
|
16403
|
+
} catch (error) {
|
|
16404
|
+
console.error(`[MLPlugin] Failed to load training history for "${modelName}":`, error.message);
|
|
16405
|
+
return null;
|
|
16406
|
+
}
|
|
16407
|
+
}
|
|
16408
|
+
/**
|
|
16409
|
+
* Compare metrics between two versions
|
|
16410
|
+
* @param {string} modelName - Model name
|
|
16411
|
+
* @param {number} version1 - First version
|
|
16412
|
+
* @param {number} version2 - Second version
|
|
16413
|
+
* @returns {Object} Comparison results
|
|
16414
|
+
*/
|
|
16415
|
+
async compareVersions(modelName, version1, version2) {
|
|
16416
|
+
if (!this.config.enableVersioning) {
|
|
16417
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16418
|
+
}
|
|
16419
|
+
try {
|
|
16420
|
+
const storage = this.getStorage();
|
|
16421
|
+
const modelConfig = this.config.models[modelName];
|
|
16422
|
+
const resourceName = modelConfig.resource;
|
|
16423
|
+
const [ok1, err1, v1Data] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version1}`)));
|
|
16424
|
+
const [ok2, err2, v2Data] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version2}`)));
|
|
16425
|
+
if (!ok1 || !v1Data) {
|
|
16426
|
+
throw new MLError(`Version ${version1} not found`, { modelName, version: version1 });
|
|
16427
|
+
}
|
|
16428
|
+
if (!ok2 || !v2Data) {
|
|
16429
|
+
throw new MLError(`Version ${version2} not found`, { modelName, version: version2 });
|
|
16430
|
+
}
|
|
16431
|
+
const metrics1 = v1Data.metrics ? JSON.parse(v1Data.metrics) : {};
|
|
16432
|
+
const metrics2 = v2Data.metrics ? JSON.parse(v2Data.metrics) : {};
|
|
16433
|
+
return {
|
|
16434
|
+
modelName,
|
|
16435
|
+
version1: {
|
|
16436
|
+
version: version1,
|
|
16437
|
+
savedAt: v1Data.savedAt,
|
|
16438
|
+
metrics: metrics1
|
|
16439
|
+
},
|
|
16440
|
+
version2: {
|
|
16441
|
+
version: version2,
|
|
16442
|
+
savedAt: v2Data.savedAt,
|
|
16443
|
+
metrics: metrics2
|
|
16444
|
+
},
|
|
16445
|
+
improvement: {
|
|
16446
|
+
loss: metrics1.loss && metrics2.loss ? ((metrics1.loss - metrics2.loss) / metrics1.loss * 100).toFixed(2) + "%" : "N/A",
|
|
16447
|
+
accuracy: metrics1.accuracy && metrics2.accuracy ? ((metrics2.accuracy - metrics1.accuracy) / metrics1.accuracy * 100).toFixed(2) + "%" : "N/A"
|
|
16448
|
+
}
|
|
16449
|
+
};
|
|
16450
|
+
} catch (error) {
|
|
16451
|
+
console.error(`[MLPlugin] Failed to compare versions for "${modelName}":`, error.message);
|
|
16452
|
+
throw error;
|
|
16453
|
+
}
|
|
16454
|
+
}
|
|
16455
|
+
/**
|
|
16456
|
+
* Rollback to a previous version
|
|
16457
|
+
* @param {string} modelName - Model name
|
|
16458
|
+
* @param {number} version - Version to rollback to (defaults to previous version)
|
|
16459
|
+
* @returns {Object} Rollback info
|
|
16460
|
+
*/
|
|
16461
|
+
async rollbackVersion(modelName, version = null) {
|
|
16462
|
+
if (!this.config.enableVersioning) {
|
|
16463
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16464
|
+
}
|
|
16465
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
16466
|
+
if (!versionInfo) {
|
|
16467
|
+
throw new MLError(`No version info found for model "${modelName}"`, { modelName });
|
|
16468
|
+
}
|
|
16469
|
+
const targetVersion = version !== null ? version : Math.max(1, versionInfo.currentVersion - 1);
|
|
16470
|
+
if (targetVersion === versionInfo.currentVersion) {
|
|
16471
|
+
throw new MLError("Cannot rollback to the same version", { modelName, version: targetVersion });
|
|
16472
|
+
}
|
|
16473
|
+
if (targetVersion < 1 || targetVersion > versionInfo.latestVersion) {
|
|
16474
|
+
throw new MLError(`Invalid version ${targetVersion}`, { modelName, version: targetVersion, latestVersion: versionInfo.latestVersion });
|
|
16475
|
+
}
|
|
16476
|
+
const result = await this.setActiveVersion(modelName, targetVersion);
|
|
16477
|
+
if (this.config.verbose) {
|
|
16478
|
+
console.log(`[MLPlugin] Rolled back model "${modelName}" from v${versionInfo.currentVersion} to v${targetVersion}`);
|
|
16479
|
+
}
|
|
16480
|
+
return {
|
|
16481
|
+
modelName,
|
|
16482
|
+
previousVersion: versionInfo.currentVersion,
|
|
16483
|
+
currentVersion: targetVersion,
|
|
16484
|
+
...result
|
|
16485
|
+
};
|
|
16486
|
+
}
|
|
13672
16487
|
}
|
|
13673
16488
|
|
|
13674
16489
|
class SqsConsumer {
|
|
@@ -14062,7 +16877,7 @@ class RelationPlugin extends Plugin {
|
|
|
14062
16877
|
if (this.verbose) {
|
|
14063
16878
|
console.log(`[RelationPlugin] Installed with ${Object.keys(this.relations).length} resources`);
|
|
14064
16879
|
}
|
|
14065
|
-
this.emit("installed", {
|
|
16880
|
+
this.emit("db:plugin:installed", {
|
|
14066
16881
|
plugin: "RelationPlugin",
|
|
14067
16882
|
resources: Object.keys(this.relations)
|
|
14068
16883
|
});
|
|
@@ -17637,7 +20452,7 @@ class S3Client extends EventEmitter {
|
|
|
17637
20452
|
return client;
|
|
17638
20453
|
}
|
|
17639
20454
|
async sendCommand(command) {
|
|
17640
|
-
this.emit("
|
|
20455
|
+
this.emit("cl:request", command.constructor.name, command.input);
|
|
17641
20456
|
const [ok, err, response] = await tryFn(() => this.client.send(command));
|
|
17642
20457
|
if (!ok) {
|
|
17643
20458
|
const bucket = this.config.bucket;
|
|
@@ -17649,7 +20464,7 @@ class S3Client extends EventEmitter {
|
|
|
17649
20464
|
commandInput: command.input
|
|
17650
20465
|
});
|
|
17651
20466
|
}
|
|
17652
|
-
this.emit("
|
|
20467
|
+
this.emit("cl:response", command.constructor.name, response, command.input);
|
|
17653
20468
|
return response;
|
|
17654
20469
|
}
|
|
17655
20470
|
async putObject({ key, metadata, contentType, body, contentEncoding, contentLength, ifMatch }) {
|
|
@@ -17674,7 +20489,7 @@ class S3Client extends EventEmitter {
|
|
|
17674
20489
|
if (contentLength !== void 0) options.ContentLength = contentLength;
|
|
17675
20490
|
if (ifMatch !== void 0) options.IfMatch = ifMatch;
|
|
17676
20491
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new PutObjectCommand(options)));
|
|
17677
|
-
this.emit("
|
|
20492
|
+
this.emit("cl:PutObject", err || response, { key, metadata, contentType, body, contentEncoding, contentLength });
|
|
17678
20493
|
if (!ok) {
|
|
17679
20494
|
throw mapAwsError(err, {
|
|
17680
20495
|
bucket: this.config.bucket,
|
|
@@ -17702,7 +20517,7 @@ class S3Client extends EventEmitter {
|
|
|
17702
20517
|
}
|
|
17703
20518
|
return res;
|
|
17704
20519
|
});
|
|
17705
|
-
this.emit("
|
|
20520
|
+
this.emit("cl:GetObject", err || response, { key });
|
|
17706
20521
|
if (!ok) {
|
|
17707
20522
|
throw mapAwsError(err, {
|
|
17708
20523
|
bucket: this.config.bucket,
|
|
@@ -17730,7 +20545,7 @@ class S3Client extends EventEmitter {
|
|
|
17730
20545
|
}
|
|
17731
20546
|
return res;
|
|
17732
20547
|
});
|
|
17733
|
-
this.emit("
|
|
20548
|
+
this.emit("cl:HeadObject", err || response, { key });
|
|
17734
20549
|
if (!ok) {
|
|
17735
20550
|
throw mapAwsError(err, {
|
|
17736
20551
|
bucket: this.config.bucket,
|
|
@@ -17763,7 +20578,7 @@ class S3Client extends EventEmitter {
|
|
|
17763
20578
|
options.ContentType = contentType;
|
|
17764
20579
|
}
|
|
17765
20580
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new CopyObjectCommand(options)));
|
|
17766
|
-
this.emit("
|
|
20581
|
+
this.emit("cl:CopyObject", err || response, { from, to, metadataDirective });
|
|
17767
20582
|
if (!ok) {
|
|
17768
20583
|
throw mapAwsError(err, {
|
|
17769
20584
|
bucket: this.config.bucket,
|
|
@@ -17788,7 +20603,7 @@ class S3Client extends EventEmitter {
|
|
|
17788
20603
|
Key: keyPrefix ? path$1.join(keyPrefix, key) : key
|
|
17789
20604
|
};
|
|
17790
20605
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new DeleteObjectCommand(options)));
|
|
17791
|
-
this.emit("
|
|
20606
|
+
this.emit("cl:DeleteObject", err || response, { key });
|
|
17792
20607
|
if (!ok) {
|
|
17793
20608
|
throw mapAwsError(err, {
|
|
17794
20609
|
bucket: this.config.bucket,
|
|
@@ -17828,7 +20643,7 @@ class S3Client extends EventEmitter {
|
|
|
17828
20643
|
deleted: results,
|
|
17829
20644
|
notFound: errors
|
|
17830
20645
|
};
|
|
17831
|
-
this.emit("
|
|
20646
|
+
this.emit("cl:DeleteObjects", report, keys);
|
|
17832
20647
|
return report;
|
|
17833
20648
|
}
|
|
17834
20649
|
/**
|
|
@@ -17858,7 +20673,7 @@ class S3Client extends EventEmitter {
|
|
|
17858
20673
|
const deleteResponse = await this.client.send(deleteCommand);
|
|
17859
20674
|
const deletedCount = deleteResponse.Deleted ? deleteResponse.Deleted.length : 0;
|
|
17860
20675
|
totalDeleted += deletedCount;
|
|
17861
|
-
this.emit("
|
|
20676
|
+
this.emit("cl:DeleteAll", {
|
|
17862
20677
|
prefix,
|
|
17863
20678
|
batch: deletedCount,
|
|
17864
20679
|
total: totalDeleted
|
|
@@ -17866,7 +20681,7 @@ class S3Client extends EventEmitter {
|
|
|
17866
20681
|
}
|
|
17867
20682
|
continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : void 0;
|
|
17868
20683
|
} while (continuationToken);
|
|
17869
|
-
this.emit("
|
|
20684
|
+
this.emit("cl:DeleteAllComplete", {
|
|
17870
20685
|
prefix,
|
|
17871
20686
|
totalDeleted
|
|
17872
20687
|
});
|
|
@@ -17897,7 +20712,7 @@ class S3Client extends EventEmitter {
|
|
|
17897
20712
|
if (!ok) {
|
|
17898
20713
|
throw new UnknownError("Unknown error in listObjects", { prefix, bucket: this.config.bucket, original: err });
|
|
17899
20714
|
}
|
|
17900
|
-
this.emit("
|
|
20715
|
+
this.emit("cl:ListObjects", response, options);
|
|
17901
20716
|
return response;
|
|
17902
20717
|
}
|
|
17903
20718
|
async count({ prefix } = {}) {
|
|
@@ -17914,7 +20729,7 @@ class S3Client extends EventEmitter {
|
|
|
17914
20729
|
truncated = response.IsTruncated || false;
|
|
17915
20730
|
continuationToken = response.NextContinuationToken;
|
|
17916
20731
|
}
|
|
17917
|
-
this.emit("
|
|
20732
|
+
this.emit("cl:Count", count, { prefix });
|
|
17918
20733
|
return count;
|
|
17919
20734
|
}
|
|
17920
20735
|
async getAllKeys({ prefix } = {}) {
|
|
@@ -17936,7 +20751,7 @@ class S3Client extends EventEmitter {
|
|
|
17936
20751
|
if (this.config.keyPrefix) {
|
|
17937
20752
|
keys = keys.map((x) => x.replace(this.config.keyPrefix, "")).map((x) => x.startsWith("/") ? x.replace(`/`, "") : x);
|
|
17938
20753
|
}
|
|
17939
|
-
this.emit("
|
|
20754
|
+
this.emit("cl:GetAllKeys", keys, { prefix });
|
|
17940
20755
|
return keys;
|
|
17941
20756
|
}
|
|
17942
20757
|
async getContinuationTokenAfterOffset(params = {}) {
|
|
@@ -17965,7 +20780,7 @@ class S3Client extends EventEmitter {
|
|
|
17965
20780
|
break;
|
|
17966
20781
|
}
|
|
17967
20782
|
}
|
|
17968
|
-
this.emit("
|
|
20783
|
+
this.emit("cl:GetContinuationTokenAfterOffset", continuationToken || null, params);
|
|
17969
20784
|
return continuationToken || null;
|
|
17970
20785
|
}
|
|
17971
20786
|
async getKeysPage(params = {}) {
|
|
@@ -17983,7 +20798,7 @@ class S3Client extends EventEmitter {
|
|
|
17983
20798
|
offset
|
|
17984
20799
|
});
|
|
17985
20800
|
if (!continuationToken) {
|
|
17986
|
-
this.emit("
|
|
20801
|
+
this.emit("cl:GetKeysPage", [], params);
|
|
17987
20802
|
return [];
|
|
17988
20803
|
}
|
|
17989
20804
|
}
|
|
@@ -18006,7 +20821,7 @@ class S3Client extends EventEmitter {
|
|
|
18006
20821
|
if (this.config.keyPrefix) {
|
|
18007
20822
|
keys = keys.map((x) => x.replace(this.config.keyPrefix, "")).map((x) => x.startsWith("/") ? x.replace(`/`, "") : x);
|
|
18008
20823
|
}
|
|
18009
|
-
this.emit("
|
|
20824
|
+
this.emit("cl:GetKeysPage", keys, params);
|
|
18010
20825
|
return keys;
|
|
18011
20826
|
}
|
|
18012
20827
|
async moveAllObjects({ prefixFrom, prefixTo }) {
|
|
@@ -18024,7 +20839,7 @@ class S3Client extends EventEmitter {
|
|
|
18024
20839
|
}
|
|
18025
20840
|
return to;
|
|
18026
20841
|
});
|
|
18027
|
-
this.emit("
|
|
20842
|
+
this.emit("cl:MoveAllObjects", { results, errors }, { prefixFrom, prefixTo });
|
|
18028
20843
|
if (errors.length > 0) {
|
|
18029
20844
|
throw new UnknownError("Some objects could not be moved", {
|
|
18030
20845
|
bucket: this.config.bucket,
|
|
@@ -20879,6 +23694,20 @@ ${errorDetails}`,
|
|
|
20879
23694
|
if (typeof body === "object") return Buffer.byteLength(JSON.stringify(body), "utf8");
|
|
20880
23695
|
return Buffer.byteLength(String(body), "utf8");
|
|
20881
23696
|
}
|
|
23697
|
+
/**
|
|
23698
|
+
* Emit standardized events with optional ID-specific variant
|
|
23699
|
+
*
|
|
23700
|
+
* @private
|
|
23701
|
+
* @param {string} event - Event name
|
|
23702
|
+
* @param {Object} payload - Event payload
|
|
23703
|
+
* @param {string} [id] - Optional ID for ID-specific events
|
|
23704
|
+
*/
|
|
23705
|
+
_emitStandardized(event, payload, id = null) {
|
|
23706
|
+
this.emit(event, payload);
|
|
23707
|
+
if (id) {
|
|
23708
|
+
this.emit(`${event}:${id}`, payload);
|
|
23709
|
+
}
|
|
23710
|
+
}
|
|
20882
23711
|
/**
|
|
20883
23712
|
* Insert a new resource object
|
|
20884
23713
|
* @param {Object} attributes - Resource attributes
|
|
@@ -21019,11 +23848,11 @@ ${errorDetails}`,
|
|
|
21019
23848
|
for (const hook of nonPartitionHooks) {
|
|
21020
23849
|
finalResult = await hook(finalResult);
|
|
21021
23850
|
}
|
|
21022
|
-
this.
|
|
23851
|
+
this._emitStandardized("inserted", finalResult, finalResult?.id || insertedObject?.id);
|
|
21023
23852
|
return finalResult;
|
|
21024
23853
|
} else {
|
|
21025
23854
|
const finalResult = await this.executeHooks("afterInsert", insertedObject);
|
|
21026
|
-
this.
|
|
23855
|
+
this._emitStandardized("inserted", finalResult, finalResult?.id || insertedObject?.id);
|
|
21027
23856
|
return finalResult;
|
|
21028
23857
|
}
|
|
21029
23858
|
}
|
|
@@ -21087,7 +23916,7 @@ ${errorDetails}`,
|
|
|
21087
23916
|
data = await this.applyVersionMapping(data, objectVersion, this.version);
|
|
21088
23917
|
}
|
|
21089
23918
|
data = await this.executeHooks("afterGet", data);
|
|
21090
|
-
this.
|
|
23919
|
+
this._emitWithDeprecation("get", "fetched", data, data.id);
|
|
21091
23920
|
const value = data;
|
|
21092
23921
|
return value;
|
|
21093
23922
|
}
|
|
@@ -21338,19 +24167,19 @@ ${errorDetails}`,
|
|
|
21338
24167
|
for (const hook of nonPartitionHooks) {
|
|
21339
24168
|
finalResult = await hook(finalResult);
|
|
21340
24169
|
}
|
|
21341
|
-
this.
|
|
24170
|
+
this._emitStandardized("updated", {
|
|
21342
24171
|
...updatedData,
|
|
21343
24172
|
$before: { ...originalData },
|
|
21344
24173
|
$after: { ...finalResult }
|
|
21345
|
-
});
|
|
24174
|
+
}, updatedData.id);
|
|
21346
24175
|
return finalResult;
|
|
21347
24176
|
} else {
|
|
21348
24177
|
const finalResult = await this.executeHooks("afterUpdate", updatedData);
|
|
21349
|
-
this.
|
|
24178
|
+
this._emitStandardized("updated", {
|
|
21350
24179
|
...updatedData,
|
|
21351
24180
|
$before: { ...originalData },
|
|
21352
24181
|
$after: { ...finalResult }
|
|
21353
|
-
});
|
|
24182
|
+
}, updatedData.id);
|
|
21354
24183
|
return finalResult;
|
|
21355
24184
|
}
|
|
21356
24185
|
}
|
|
@@ -21749,11 +24578,11 @@ ${errorDetails}`,
|
|
|
21749
24578
|
for (const hook of nonPartitionHooks) {
|
|
21750
24579
|
finalResult = await hook(finalResult);
|
|
21751
24580
|
}
|
|
21752
|
-
this.
|
|
24581
|
+
this._emitStandardized("updated", {
|
|
21753
24582
|
...updatedData,
|
|
21754
24583
|
$before: { ...originalData },
|
|
21755
24584
|
$after: { ...finalResult }
|
|
21756
|
-
});
|
|
24585
|
+
}, updatedData.id);
|
|
21757
24586
|
return {
|
|
21758
24587
|
success: true,
|
|
21759
24588
|
data: finalResult,
|
|
@@ -21762,11 +24591,11 @@ ${errorDetails}`,
|
|
|
21762
24591
|
} else {
|
|
21763
24592
|
await this.handlePartitionReferenceUpdates(oldData, newData);
|
|
21764
24593
|
const finalResult = await this.executeHooks("afterUpdate", updatedData);
|
|
21765
|
-
this.
|
|
24594
|
+
this._emitStandardized("updated", {
|
|
21766
24595
|
...updatedData,
|
|
21767
24596
|
$before: { ...originalData },
|
|
21768
24597
|
$after: { ...finalResult }
|
|
21769
|
-
});
|
|
24598
|
+
}, updatedData.id);
|
|
21770
24599
|
return {
|
|
21771
24600
|
success: true,
|
|
21772
24601
|
data: finalResult,
|
|
@@ -21797,11 +24626,11 @@ ${errorDetails}`,
|
|
|
21797
24626
|
await this.executeHooks("beforeDelete", objectData);
|
|
21798
24627
|
const key = this.getResourceKey(id);
|
|
21799
24628
|
const [ok2, err2, response] = await tryFn(() => this.client.deleteObject(key));
|
|
21800
|
-
this.
|
|
24629
|
+
this._emitWithDeprecation("delete", "deleted", {
|
|
21801
24630
|
...objectData,
|
|
21802
24631
|
$before: { ...objectData },
|
|
21803
24632
|
$after: null
|
|
21804
|
-
});
|
|
24633
|
+
}, id);
|
|
21805
24634
|
if (deleteError) {
|
|
21806
24635
|
throw mapAwsError(deleteError, {
|
|
21807
24636
|
bucket: this.client.config.bucket,
|
|
@@ -21925,7 +24754,7 @@ ${errorDetails}`,
|
|
|
21925
24754
|
}
|
|
21926
24755
|
const count = await this.client.count({ prefix });
|
|
21927
24756
|
await this.executeHooks("afterCount", { count, partition, partitionValues });
|
|
21928
|
-
this.
|
|
24757
|
+
this._emitWithDeprecation("count", "counted", count);
|
|
21929
24758
|
return count;
|
|
21930
24759
|
}
|
|
21931
24760
|
/**
|
|
@@ -21948,7 +24777,7 @@ ${errorDetails}`,
|
|
|
21948
24777
|
const result = await this.insert(attributes);
|
|
21949
24778
|
return result;
|
|
21950
24779
|
});
|
|
21951
|
-
this.
|
|
24780
|
+
this._emitWithDeprecation("insertMany", "inserted-many", objects.length);
|
|
21952
24781
|
return results;
|
|
21953
24782
|
}
|
|
21954
24783
|
/**
|
|
@@ -21983,7 +24812,7 @@ ${errorDetails}`,
|
|
|
21983
24812
|
return response;
|
|
21984
24813
|
});
|
|
21985
24814
|
await this.executeHooks("afterDeleteMany", { ids, results });
|
|
21986
|
-
this.
|
|
24815
|
+
this._emitWithDeprecation("deleteMany", "deleted-many", ids.length);
|
|
21987
24816
|
return results;
|
|
21988
24817
|
}
|
|
21989
24818
|
async deleteAll() {
|
|
@@ -21992,7 +24821,7 @@ ${errorDetails}`,
|
|
|
21992
24821
|
}
|
|
21993
24822
|
const prefix = `resource=${this.name}/data`;
|
|
21994
24823
|
const deletedCount = await this.client.deleteAll({ prefix });
|
|
21995
|
-
this.
|
|
24824
|
+
this._emitWithDeprecation("deleteAll", "deleted-all", {
|
|
21996
24825
|
version: this.version,
|
|
21997
24826
|
prefix,
|
|
21998
24827
|
deletedCount
|
|
@@ -22009,7 +24838,7 @@ ${errorDetails}`,
|
|
|
22009
24838
|
}
|
|
22010
24839
|
const prefix = `resource=${this.name}`;
|
|
22011
24840
|
const deletedCount = await this.client.deleteAll({ prefix });
|
|
22012
|
-
this.
|
|
24841
|
+
this._emitWithDeprecation("deleteAllData", "deleted-all-data", {
|
|
22013
24842
|
resource: this.name,
|
|
22014
24843
|
prefix,
|
|
22015
24844
|
deletedCount
|
|
@@ -22079,7 +24908,7 @@ ${errorDetails}`,
|
|
|
22079
24908
|
const idPart = parts.find((part) => part.startsWith("id="));
|
|
22080
24909
|
return idPart ? idPart.replace("id=", "") : null;
|
|
22081
24910
|
}).filter(Boolean);
|
|
22082
|
-
this.
|
|
24911
|
+
this._emitWithDeprecation("listIds", "listed-ids", ids.length);
|
|
22083
24912
|
return ids;
|
|
22084
24913
|
}
|
|
22085
24914
|
/**
|
|
@@ -22121,12 +24950,12 @@ ${errorDetails}`,
|
|
|
22121
24950
|
const [ok, err, ids] = await tryFn(() => this.listIds({ limit, offset }));
|
|
22122
24951
|
if (!ok) throw err;
|
|
22123
24952
|
const results = await this.processListResults(ids, "main");
|
|
22124
|
-
this.
|
|
24953
|
+
this._emitWithDeprecation("list", "listed", { count: results.length, errors: 0 });
|
|
22125
24954
|
return results;
|
|
22126
24955
|
}
|
|
22127
24956
|
async listPartition({ partition, partitionValues, limit, offset = 0 }) {
|
|
22128
24957
|
if (!this.config.partitions?.[partition]) {
|
|
22129
|
-
this.
|
|
24958
|
+
this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 0 });
|
|
22130
24959
|
return [];
|
|
22131
24960
|
}
|
|
22132
24961
|
const partitionDef = this.config.partitions[partition];
|
|
@@ -22136,7 +24965,7 @@ ${errorDetails}`,
|
|
|
22136
24965
|
const ids = this.extractIdsFromKeys(keys).slice(offset);
|
|
22137
24966
|
const filteredIds = limit ? ids.slice(0, limit) : ids;
|
|
22138
24967
|
const results = await this.processPartitionResults(filteredIds, partition, partitionDef, keys);
|
|
22139
|
-
this.
|
|
24968
|
+
this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: results.length, errors: 0 });
|
|
22140
24969
|
return results;
|
|
22141
24970
|
}
|
|
22142
24971
|
/**
|
|
@@ -22181,7 +25010,7 @@ ${errorDetails}`,
|
|
|
22181
25010
|
}
|
|
22182
25011
|
return this.handleResourceError(err, id, context);
|
|
22183
25012
|
});
|
|
22184
|
-
this.
|
|
25013
|
+
this._emitWithDeprecation("list", "listed", { count: results.length, errors: 0 });
|
|
22185
25014
|
return results;
|
|
22186
25015
|
}
|
|
22187
25016
|
/**
|
|
@@ -22244,10 +25073,10 @@ ${errorDetails}`,
|
|
|
22244
25073
|
*/
|
|
22245
25074
|
handleListError(error, { partition, partitionValues }) {
|
|
22246
25075
|
if (error.message.includes("Partition '") && error.message.includes("' not found")) {
|
|
22247
|
-
this.
|
|
25076
|
+
this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
|
|
22248
25077
|
return [];
|
|
22249
25078
|
}
|
|
22250
|
-
this.
|
|
25079
|
+
this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
|
|
22251
25080
|
return [];
|
|
22252
25081
|
}
|
|
22253
25082
|
/**
|
|
@@ -22280,7 +25109,7 @@ ${errorDetails}`,
|
|
|
22280
25109
|
throw err;
|
|
22281
25110
|
});
|
|
22282
25111
|
const finalResults = await this.executeHooks("afterGetMany", results);
|
|
22283
|
-
this.
|
|
25112
|
+
this._emitWithDeprecation("getMany", "fetched-many", ids.length);
|
|
22284
25113
|
return finalResults;
|
|
22285
25114
|
}
|
|
22286
25115
|
/**
|
|
@@ -22366,7 +25195,7 @@ ${errorDetails}`,
|
|
|
22366
25195
|
hasTotalItems: totalItems !== null
|
|
22367
25196
|
}
|
|
22368
25197
|
};
|
|
22369
|
-
this.
|
|
25198
|
+
this._emitWithDeprecation("page", "paginated", result2);
|
|
22370
25199
|
return result2;
|
|
22371
25200
|
});
|
|
22372
25201
|
if (ok) return result;
|
|
@@ -22436,7 +25265,7 @@ ${errorDetails}`,
|
|
|
22436
25265
|
contentType
|
|
22437
25266
|
}));
|
|
22438
25267
|
if (!ok2) throw err2;
|
|
22439
|
-
this.
|
|
25268
|
+
this._emitWithDeprecation("setContent", "content-set", { id, contentType, contentLength: buffer.length }, id);
|
|
22440
25269
|
return updatedData;
|
|
22441
25270
|
}
|
|
22442
25271
|
/**
|
|
@@ -22465,7 +25294,7 @@ ${errorDetails}`,
|
|
|
22465
25294
|
}
|
|
22466
25295
|
const buffer = Buffer.from(await response.Body.transformToByteArray());
|
|
22467
25296
|
const contentType = response.ContentType || null;
|
|
22468
|
-
this.
|
|
25297
|
+
this._emitWithDeprecation("content", "content-fetched", { id, contentLength: buffer.length, contentType }, id);
|
|
22469
25298
|
return {
|
|
22470
25299
|
buffer,
|
|
22471
25300
|
contentType
|
|
@@ -22497,7 +25326,7 @@ ${errorDetails}`,
|
|
|
22497
25326
|
metadata: existingMetadata
|
|
22498
25327
|
}));
|
|
22499
25328
|
if (!ok2) throw err2;
|
|
22500
|
-
this.
|
|
25329
|
+
this._emitWithDeprecation("deleteContent", "content-deleted", id, id);
|
|
22501
25330
|
return response;
|
|
22502
25331
|
}
|
|
22503
25332
|
/**
|
|
@@ -22809,7 +25638,7 @@ ${errorDetails}`,
|
|
|
22809
25638
|
const data = await this.get(id);
|
|
22810
25639
|
data._partition = partitionName;
|
|
22811
25640
|
data._partitionValues = partitionValues;
|
|
22812
|
-
this.
|
|
25641
|
+
this._emitWithDeprecation("getFromPartition", "partition-fetched", data, data.id);
|
|
22813
25642
|
return data;
|
|
22814
25643
|
}
|
|
22815
25644
|
/**
|
|
@@ -23218,7 +26047,7 @@ class Database extends EventEmitter {
|
|
|
23218
26047
|
})();
|
|
23219
26048
|
this.version = "1";
|
|
23220
26049
|
this.s3dbVersion = (() => {
|
|
23221
|
-
const [ok, err, version] = tryFn(() => true ? "13.
|
|
26050
|
+
const [ok, err, version] = tryFn(() => true ? "13.2.1" : "latest");
|
|
23222
26051
|
return ok ? version : "latest";
|
|
23223
26052
|
})();
|
|
23224
26053
|
this._resourcesMap = {};
|
|
@@ -23388,12 +26217,12 @@ class Database extends EventEmitter {
|
|
|
23388
26217
|
}
|
|
23389
26218
|
}
|
|
23390
26219
|
if (definitionChanges.length > 0) {
|
|
23391
|
-
this.emit("
|
|
26220
|
+
this.emit("db:resource-definitions-changed", {
|
|
23392
26221
|
changes: definitionChanges,
|
|
23393
26222
|
metadata: this.savedMetadata
|
|
23394
26223
|
});
|
|
23395
26224
|
}
|
|
23396
|
-
this.emit("connected", /* @__PURE__ */ new Date());
|
|
26225
|
+
this.emit("db:connected", /* @__PURE__ */ new Date());
|
|
23397
26226
|
}
|
|
23398
26227
|
/**
|
|
23399
26228
|
* Detect changes in resource definitions compared to saved metadata
|
|
@@ -23607,7 +26436,7 @@ class Database extends EventEmitter {
|
|
|
23607
26436
|
if (index > -1) {
|
|
23608
26437
|
this.pluginList.splice(index, 1);
|
|
23609
26438
|
}
|
|
23610
|
-
this.emit("plugin
|
|
26439
|
+
this.emit("db:plugin:uninstalled", { name: pluginName, plugin });
|
|
23611
26440
|
}
|
|
23612
26441
|
async uploadMetadataFile() {
|
|
23613
26442
|
const metadata = {
|
|
@@ -23666,7 +26495,7 @@ class Database extends EventEmitter {
|
|
|
23666
26495
|
contentType: "application/json"
|
|
23667
26496
|
});
|
|
23668
26497
|
this.savedMetadata = metadata;
|
|
23669
|
-
this.emit("
|
|
26498
|
+
this.emit("db:metadata-uploaded", metadata);
|
|
23670
26499
|
}
|
|
23671
26500
|
blankMetadataStructure() {
|
|
23672
26501
|
return {
|
|
@@ -23923,7 +26752,7 @@ class Database extends EventEmitter {
|
|
|
23923
26752
|
body: JSON.stringify(metadata, null, 2),
|
|
23924
26753
|
contentType: "application/json"
|
|
23925
26754
|
});
|
|
23926
|
-
this.emit("
|
|
26755
|
+
this.emit("db:metadata-healed", { healingLog, metadata });
|
|
23927
26756
|
if (this.verbose) {
|
|
23928
26757
|
console.warn("S3DB: Successfully uploaded healed metadata");
|
|
23929
26758
|
}
|
|
@@ -24063,7 +26892,7 @@ class Database extends EventEmitter {
|
|
|
24063
26892
|
if (!existingVersionData || existingVersionData.hash !== newHash) {
|
|
24064
26893
|
await this.uploadMetadataFile();
|
|
24065
26894
|
}
|
|
24066
|
-
this.emit("
|
|
26895
|
+
this.emit("db:resource:updated", name);
|
|
24067
26896
|
return existingResource;
|
|
24068
26897
|
}
|
|
24069
26898
|
const existingMetadata = this.savedMetadata?.resources?.[name];
|
|
@@ -24100,7 +26929,7 @@ class Database extends EventEmitter {
|
|
|
24100
26929
|
this._applyMiddlewares(resource, middlewares);
|
|
24101
26930
|
}
|
|
24102
26931
|
await this.uploadMetadataFile();
|
|
24103
|
-
this.emit("
|
|
26932
|
+
this.emit("db:resource:created", name);
|
|
24104
26933
|
return resource;
|
|
24105
26934
|
}
|
|
24106
26935
|
/**
|
|
@@ -24264,7 +27093,7 @@ class Database extends EventEmitter {
|
|
|
24264
27093
|
if (this.client && typeof this.client.removeAllListeners === "function") {
|
|
24265
27094
|
this.client.removeAllListeners();
|
|
24266
27095
|
}
|
|
24267
|
-
await this.emit("disconnected", /* @__PURE__ */ new Date());
|
|
27096
|
+
await this.emit("db:disconnected", /* @__PURE__ */ new Date());
|
|
24268
27097
|
this.removeAllListeners();
|
|
24269
27098
|
if (this._exitListener && typeof process !== "undefined") {
|
|
24270
27099
|
process.off("exit", this._exitListener);
|
|
@@ -24376,7 +27205,7 @@ class Database extends EventEmitter {
|
|
|
24376
27205
|
for (const hook of hooks) {
|
|
24377
27206
|
const [ok, error] = await tryFn(() => hook({ database: this, ...context }));
|
|
24378
27207
|
if (!ok) {
|
|
24379
|
-
this.emit("
|
|
27208
|
+
this.emit("db:hook-error", { event, error, context });
|
|
24380
27209
|
if (this.strictHooks) {
|
|
24381
27210
|
throw new DatabaseError(`Hook execution failed for event '${event}': ${error.message}`, {
|
|
24382
27211
|
event,
|
|
@@ -25952,7 +28781,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
25952
28781
|
if (this.config.verbose) {
|
|
25953
28782
|
console.warn(`[ReplicatorPlugin] Insert event failed for resource ${resource.name}: ${error.message}`);
|
|
25954
28783
|
}
|
|
25955
|
-
this.emit("error", { operation: "insert", error: error.message, resource: resource.name });
|
|
28784
|
+
this.emit("plg:replicator:error", { operation: "insert", error: error.message, resource: resource.name });
|
|
25956
28785
|
}
|
|
25957
28786
|
};
|
|
25958
28787
|
const updateHandler = async (data, beforeData) => {
|
|
@@ -25965,7 +28794,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
25965
28794
|
if (this.config.verbose) {
|
|
25966
28795
|
console.warn(`[ReplicatorPlugin] Update event failed for resource ${resource.name}: ${error.message}`);
|
|
25967
28796
|
}
|
|
25968
|
-
this.emit("error", { operation: "update", error: error.message, resource: resource.name });
|
|
28797
|
+
this.emit("plg:replicator:error", { operation: "update", error: error.message, resource: resource.name });
|
|
25969
28798
|
}
|
|
25970
28799
|
};
|
|
25971
28800
|
const deleteHandler = async (data) => {
|
|
@@ -25976,7 +28805,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
25976
28805
|
if (this.config.verbose) {
|
|
25977
28806
|
console.warn(`[ReplicatorPlugin] Delete event failed for resource ${resource.name}: ${error.message}`);
|
|
25978
28807
|
}
|
|
25979
|
-
this.emit("error", { operation: "delete", error: error.message, resource: resource.name });
|
|
28808
|
+
this.emit("plg:replicator:error", { operation: "delete", error: error.message, resource: resource.name });
|
|
25980
28809
|
}
|
|
25981
28810
|
};
|
|
25982
28811
|
this.eventHandlers.set(resource.name, {
|
|
@@ -26097,7 +28926,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26097
28926
|
if (this.config.verbose) {
|
|
26098
28927
|
console.warn(`[ReplicatorPlugin] Failed to log error for ${resourceName}: ${logError.message}`);
|
|
26099
28928
|
}
|
|
26100
|
-
this.emit("
|
|
28929
|
+
this.emit("plg:replicator:log-error", {
|
|
26101
28930
|
replicator: replicator.name || replicator.id,
|
|
26102
28931
|
resourceName,
|
|
26103
28932
|
operation,
|
|
@@ -26122,7 +28951,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26122
28951
|
() => replicator.replicate(resourceName, operation, data, recordId, beforeData),
|
|
26123
28952
|
this.config.maxRetries
|
|
26124
28953
|
);
|
|
26125
|
-
this.emit("replicated", {
|
|
28954
|
+
this.emit("plg:replicator:replicated", {
|
|
26126
28955
|
replicator: replicator.name || replicator.id,
|
|
26127
28956
|
resourceName,
|
|
26128
28957
|
operation,
|
|
@@ -26138,7 +28967,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26138
28967
|
if (this.config.verbose) {
|
|
26139
28968
|
console.warn(`[ReplicatorPlugin] Replication failed for ${replicator.name || replicator.id} on ${resourceName}: ${error.message}`);
|
|
26140
28969
|
}
|
|
26141
|
-
this.emit("
|
|
28970
|
+
this.emit("plg:replicator:error", {
|
|
26142
28971
|
replicator: replicator.name || replicator.id,
|
|
26143
28972
|
resourceName,
|
|
26144
28973
|
operation,
|
|
@@ -26170,7 +28999,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26170
28999
|
if (this.config.verbose) {
|
|
26171
29000
|
console.warn(`[ReplicatorPlugin] Replicator item processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${err.message}`);
|
|
26172
29001
|
}
|
|
26173
|
-
this.emit("
|
|
29002
|
+
this.emit("plg:replicator:error", {
|
|
26174
29003
|
replicator: replicator.name || replicator.id,
|
|
26175
29004
|
resourceName: item.resourceName,
|
|
26176
29005
|
operation: item.operation,
|
|
@@ -26182,7 +29011,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26182
29011
|
}
|
|
26183
29012
|
return { success: false, error: err.message };
|
|
26184
29013
|
}
|
|
26185
|
-
this.emit("replicated", {
|
|
29014
|
+
this.emit("plg:replicator:replicated", {
|
|
26186
29015
|
replicator: replicator.name || replicator.id,
|
|
26187
29016
|
resourceName: item.resourceName,
|
|
26188
29017
|
operation: item.operation,
|
|
@@ -26198,7 +29027,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26198
29027
|
if (this.config.verbose) {
|
|
26199
29028
|
console.warn(`[ReplicatorPlugin] Wrapper processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${wrapperError.message}`);
|
|
26200
29029
|
}
|
|
26201
|
-
this.emit("
|
|
29030
|
+
this.emit("plg:replicator:error", {
|
|
26202
29031
|
replicator: replicator.name || replicator.id,
|
|
26203
29032
|
resourceName: item.resourceName,
|
|
26204
29033
|
operation: item.operation,
|
|
@@ -26216,7 +29045,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26216
29045
|
async logReplicator(item) {
|
|
26217
29046
|
const logRes = this.replicatorLog || this.database.resources[normalizeResourceName(this.config.replicatorLogResource)];
|
|
26218
29047
|
if (!logRes) {
|
|
26219
|
-
this.emit("replicator
|
|
29048
|
+
this.emit("plg:replicator:log-failed", { error: "replicator log resource not found", item });
|
|
26220
29049
|
return;
|
|
26221
29050
|
}
|
|
26222
29051
|
const logItem = {
|
|
@@ -26234,7 +29063,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26234
29063
|
if (this.config.verbose) {
|
|
26235
29064
|
console.warn(`[ReplicatorPlugin] Failed to log replicator item: ${err.message}`);
|
|
26236
29065
|
}
|
|
26237
|
-
this.emit("replicator
|
|
29066
|
+
this.emit("plg:replicator:log-failed", { error: err, item });
|
|
26238
29067
|
}
|
|
26239
29068
|
}
|
|
26240
29069
|
async updateReplicatorLog(logId, updates) {
|
|
@@ -26246,7 +29075,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26246
29075
|
});
|
|
26247
29076
|
});
|
|
26248
29077
|
if (!ok) {
|
|
26249
|
-
this.emit("replicator
|
|
29078
|
+
this.emit("plg:replicator:update-log-failed", { error: err.message, logId, updates });
|
|
26250
29079
|
}
|
|
26251
29080
|
}
|
|
26252
29081
|
// Utility methods
|
|
@@ -26330,7 +29159,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26330
29159
|
for (const resourceName in this.database.resources) {
|
|
26331
29160
|
if (normalizeResourceName(resourceName) === normalizeResourceName("plg_replicator_logs")) continue;
|
|
26332
29161
|
if (replicator.shouldReplicateResource(resourceName)) {
|
|
26333
|
-
this.emit("replicator
|
|
29162
|
+
this.emit("plg:replicator:sync-resource", { resourceName, replicatorId });
|
|
26334
29163
|
const resource = this.database.resources[resourceName];
|
|
26335
29164
|
let offset = 0;
|
|
26336
29165
|
const pageSize = this.config.batchSize || 100;
|
|
@@ -26346,7 +29175,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26346
29175
|
}
|
|
26347
29176
|
}
|
|
26348
29177
|
}
|
|
26349
|
-
this.emit("replicator
|
|
29178
|
+
this.emit("plg:replicator:sync-completed", { replicatorId, stats: this.stats });
|
|
26350
29179
|
}
|
|
26351
29180
|
async stop() {
|
|
26352
29181
|
const [ok, error] = await tryFn(async () => {
|
|
@@ -26361,7 +29190,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26361
29190
|
if (this.config.verbose) {
|
|
26362
29191
|
console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
|
|
26363
29192
|
}
|
|
26364
|
-
this.emit("
|
|
29193
|
+
this.emit("plg:replicator:stop-error", {
|
|
26365
29194
|
replicator: replicator.name || replicator.id || "unknown",
|
|
26366
29195
|
driver: replicator.driver || "unknown",
|
|
26367
29196
|
error: replicatorError.message
|
|
@@ -26392,7 +29221,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26392
29221
|
if (this.config.verbose) {
|
|
26393
29222
|
console.warn(`[ReplicatorPlugin] Failed to stop plugin: ${error.message}`);
|
|
26394
29223
|
}
|
|
26395
|
-
this.emit("
|
|
29224
|
+
this.emit("plg:replicator:plugin-stop-error", {
|
|
26396
29225
|
error: error.message
|
|
26397
29226
|
});
|
|
26398
29227
|
}
|
|
@@ -26549,7 +29378,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26549
29378
|
if (this.config.verbose) {
|
|
26550
29379
|
console.log(`[S3QueuePlugin] Started ${concurrency} workers`);
|
|
26551
29380
|
}
|
|
26552
|
-
this.emit("workers
|
|
29381
|
+
this.emit("plg:s3-queue:workers-started", { concurrency, workerId: this.workerId });
|
|
26553
29382
|
}
|
|
26554
29383
|
async stopProcessing() {
|
|
26555
29384
|
if (!this.isRunning) return;
|
|
@@ -26564,7 +29393,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26564
29393
|
if (this.config.verbose) {
|
|
26565
29394
|
console.log("[S3QueuePlugin] Stopped all workers");
|
|
26566
29395
|
}
|
|
26567
|
-
this.emit("workers
|
|
29396
|
+
this.emit("plg:s3-queue:workers-stopped", { workerId: this.workerId });
|
|
26568
29397
|
}
|
|
26569
29398
|
createWorker(handler, workerIndex) {
|
|
26570
29399
|
return (async () => {
|
|
@@ -26732,7 +29561,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26732
29561
|
});
|
|
26733
29562
|
await this.completeMessage(message.queueId, result);
|
|
26734
29563
|
const duration = Date.now() - startTime;
|
|
26735
|
-
this.emit("message
|
|
29564
|
+
this.emit("plg:s3-queue:message-completed", {
|
|
26736
29565
|
queueId: message.queueId,
|
|
26737
29566
|
originalId: message.record.id,
|
|
26738
29567
|
duration,
|
|
@@ -26745,7 +29574,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26745
29574
|
const shouldRetry = message.attempts < message.maxAttempts;
|
|
26746
29575
|
if (shouldRetry) {
|
|
26747
29576
|
await this.retryMessage(message.queueId, message.attempts, error.message);
|
|
26748
|
-
this.emit("message
|
|
29577
|
+
this.emit("plg:s3-queue:message-retry", {
|
|
26749
29578
|
queueId: message.queueId,
|
|
26750
29579
|
originalId: message.record.id,
|
|
26751
29580
|
attempts: message.attempts,
|
|
@@ -26753,7 +29582,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26753
29582
|
});
|
|
26754
29583
|
} else {
|
|
26755
29584
|
await this.moveToDeadLetter(message.queueId, message.record, error.message);
|
|
26756
|
-
this.emit("message
|
|
29585
|
+
this.emit("plg:s3-queue:message-dead", {
|
|
26757
29586
|
queueId: message.queueId,
|
|
26758
29587
|
originalId: message.record.id,
|
|
26759
29588
|
error: error.message
|
|
@@ -26985,7 +29814,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
26985
29814
|
});
|
|
26986
29815
|
}
|
|
26987
29816
|
await this._startScheduling();
|
|
26988
|
-
this.emit("initialized", { jobs: this.jobs.size });
|
|
29817
|
+
this.emit("db:plugin:initialized", { jobs: this.jobs.size });
|
|
26989
29818
|
}
|
|
26990
29819
|
async _createJobHistoryResource() {
|
|
26991
29820
|
const [ok] = await tryFn(() => this.database.createResource({
|
|
@@ -27123,7 +29952,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27123
29952
|
if (this.config.onJobStart) {
|
|
27124
29953
|
await this._executeHook(this.config.onJobStart, jobName, context);
|
|
27125
29954
|
}
|
|
27126
|
-
this.emit("
|
|
29955
|
+
this.emit("plg:scheduler:job-start", { jobName, executionId, startTime });
|
|
27127
29956
|
let attempt = 0;
|
|
27128
29957
|
let lastError = null;
|
|
27129
29958
|
let result = null;
|
|
@@ -27190,7 +30019,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27190
30019
|
} else if (status !== "success" && this.config.onJobError) {
|
|
27191
30020
|
await this._executeHook(this.config.onJobError, jobName, lastError, attempt);
|
|
27192
30021
|
}
|
|
27193
|
-
this.emit("
|
|
30022
|
+
this.emit("plg:scheduler:job-complete", {
|
|
27194
30023
|
jobName,
|
|
27195
30024
|
executionId,
|
|
27196
30025
|
status,
|
|
@@ -27276,7 +30105,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27276
30105
|
}
|
|
27277
30106
|
job.enabled = true;
|
|
27278
30107
|
this._scheduleNextExecution(jobName);
|
|
27279
|
-
this.emit("
|
|
30108
|
+
this.emit("plg:scheduler:job-enabled", { jobName });
|
|
27280
30109
|
}
|
|
27281
30110
|
/**
|
|
27282
30111
|
* Disable a job
|
|
@@ -27297,7 +30126,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27297
30126
|
clearTimeout(timer);
|
|
27298
30127
|
this.timers.delete(jobName);
|
|
27299
30128
|
}
|
|
27300
|
-
this.emit("
|
|
30129
|
+
this.emit("plg:scheduler:job-disabled", { jobName });
|
|
27301
30130
|
}
|
|
27302
30131
|
/**
|
|
27303
30132
|
* Get job status and statistics
|
|
@@ -27435,7 +30264,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27435
30264
|
if (job.enabled) {
|
|
27436
30265
|
this._scheduleNextExecution(jobName);
|
|
27437
30266
|
}
|
|
27438
|
-
this.emit("
|
|
30267
|
+
this.emit("plg:scheduler:job-added", { jobName });
|
|
27439
30268
|
}
|
|
27440
30269
|
/**
|
|
27441
30270
|
* Remove a job
|
|
@@ -27458,7 +30287,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27458
30287
|
this.jobs.delete(jobName);
|
|
27459
30288
|
this.statistics.delete(jobName);
|
|
27460
30289
|
this.activeJobs.delete(jobName);
|
|
27461
|
-
this.emit("
|
|
30290
|
+
this.emit("plg:scheduler:job-removed", { jobName });
|
|
27462
30291
|
}
|
|
27463
30292
|
/**
|
|
27464
30293
|
* Get plugin instance by name (for job actions that need other plugins)
|
|
@@ -27499,9 +30328,14 @@ class SchedulerPlugin extends Plugin {
|
|
|
27499
30328
|
}
|
|
27500
30329
|
}
|
|
27501
30330
|
|
|
30331
|
+
var scheduler_plugin = /*#__PURE__*/Object.freeze({
|
|
30332
|
+
__proto__: null,
|
|
30333
|
+
SchedulerPlugin: SchedulerPlugin
|
|
30334
|
+
});
|
|
30335
|
+
|
|
27502
30336
|
class StateMachineError extends S3dbError {
|
|
27503
30337
|
constructor(message, details = {}) {
|
|
27504
|
-
const { currentState, targetState, resourceName, operation = "unknown", ...rest } = details;
|
|
30338
|
+
const { currentState, targetState, resourceName, operation = "unknown", retriable, ...rest } = details;
|
|
27505
30339
|
let description = details.description;
|
|
27506
30340
|
if (!description) {
|
|
27507
30341
|
description = `
|
|
@@ -27526,6 +30360,158 @@ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-mach
|
|
|
27526
30360
|
`.trim();
|
|
27527
30361
|
}
|
|
27528
30362
|
super(message, { ...rest, currentState, targetState, resourceName, operation, description });
|
|
30363
|
+
if (retriable !== void 0) {
|
|
30364
|
+
this.retriable = retriable;
|
|
30365
|
+
}
|
|
30366
|
+
}
|
|
30367
|
+
}
|
|
30368
|
+
|
|
30369
|
+
const RETRIABLE = "RETRIABLE";
|
|
30370
|
+
const NON_RETRIABLE = "NON_RETRIABLE";
|
|
30371
|
+
const RETRIABLE_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
30372
|
+
"ECONNREFUSED",
|
|
30373
|
+
"ETIMEDOUT",
|
|
30374
|
+
"ECONNRESET",
|
|
30375
|
+
"EPIPE",
|
|
30376
|
+
"ENOTFOUND",
|
|
30377
|
+
"NetworkError",
|
|
30378
|
+
"NETWORK_ERROR",
|
|
30379
|
+
"TimeoutError",
|
|
30380
|
+
"TIMEOUT"
|
|
30381
|
+
]);
|
|
30382
|
+
const RETRIABLE_AWS_CODES = /* @__PURE__ */ new Set([
|
|
30383
|
+
"ThrottlingException",
|
|
30384
|
+
"TooManyRequestsException",
|
|
30385
|
+
"RequestLimitExceeded",
|
|
30386
|
+
"ProvisionedThroughputExceededException",
|
|
30387
|
+
"RequestThrottledException",
|
|
30388
|
+
"SlowDown",
|
|
30389
|
+
"ServiceUnavailable"
|
|
30390
|
+
]);
|
|
30391
|
+
const RETRIABLE_AWS_CONFLICTS = /* @__PURE__ */ new Set([
|
|
30392
|
+
"ConditionalCheckFailedException",
|
|
30393
|
+
"TransactionConflictException"
|
|
30394
|
+
]);
|
|
30395
|
+
const RETRIABLE_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
30396
|
+
429,
|
|
30397
|
+
// Too Many Requests
|
|
30398
|
+
500,
|
|
30399
|
+
// Internal Server Error
|
|
30400
|
+
502,
|
|
30401
|
+
// Bad Gateway
|
|
30402
|
+
503,
|
|
30403
|
+
// Service Unavailable
|
|
30404
|
+
504,
|
|
30405
|
+
// Gateway Timeout
|
|
30406
|
+
507,
|
|
30407
|
+
// Insufficient Storage
|
|
30408
|
+
509
|
|
30409
|
+
// Bandwidth Limit Exceeded
|
|
30410
|
+
]);
|
|
30411
|
+
const NON_RETRIABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
30412
|
+
"ValidationError",
|
|
30413
|
+
"StateMachineError",
|
|
30414
|
+
"SchemaError",
|
|
30415
|
+
"AuthenticationError",
|
|
30416
|
+
"PermissionError",
|
|
30417
|
+
"BusinessLogicError",
|
|
30418
|
+
"InvalidStateTransition"
|
|
30419
|
+
]);
|
|
30420
|
+
const NON_RETRIABLE_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
30421
|
+
400,
|
|
30422
|
+
// Bad Request
|
|
30423
|
+
401,
|
|
30424
|
+
// Unauthorized
|
|
30425
|
+
403,
|
|
30426
|
+
// Forbidden
|
|
30427
|
+
404,
|
|
30428
|
+
// Not Found
|
|
30429
|
+
405,
|
|
30430
|
+
// Method Not Allowed
|
|
30431
|
+
406,
|
|
30432
|
+
// Not Acceptable
|
|
30433
|
+
409,
|
|
30434
|
+
// Conflict
|
|
30435
|
+
410,
|
|
30436
|
+
// Gone
|
|
30437
|
+
422
|
|
30438
|
+
// Unprocessable Entity
|
|
30439
|
+
]);
|
|
30440
|
+
class ErrorClassifier {
|
|
30441
|
+
/**
|
|
30442
|
+
* Classify an error as RETRIABLE or NON_RETRIABLE
|
|
30443
|
+
*
|
|
30444
|
+
* @param {Error} error - The error to classify
|
|
30445
|
+
* @param {Object} options - Classification options
|
|
30446
|
+
* @param {Array<string>} options.retryableErrors - Custom retriable error names/codes
|
|
30447
|
+
* @param {Array<string>} options.nonRetriableErrors - Custom non-retriable error names/codes
|
|
30448
|
+
* @returns {string} 'RETRIABLE' or 'NON_RETRIABLE'
|
|
30449
|
+
*/
|
|
30450
|
+
static classify(error, options = {}) {
|
|
30451
|
+
if (!error) return NON_RETRIABLE;
|
|
30452
|
+
const {
|
|
30453
|
+
retryableErrors = [],
|
|
30454
|
+
nonRetriableErrors = []
|
|
30455
|
+
} = options;
|
|
30456
|
+
if (retryableErrors.length > 0) {
|
|
30457
|
+
const isCustomRetriable = retryableErrors.some(
|
|
30458
|
+
(errType) => error.code === errType || error.name === errType || error.message?.includes(errType)
|
|
30459
|
+
);
|
|
30460
|
+
if (isCustomRetriable) return RETRIABLE;
|
|
30461
|
+
}
|
|
30462
|
+
if (nonRetriableErrors.length > 0) {
|
|
30463
|
+
const isCustomNonRetriable = nonRetriableErrors.some(
|
|
30464
|
+
(errType) => error.code === errType || error.name === errType || error.message?.includes(errType)
|
|
30465
|
+
);
|
|
30466
|
+
if (isCustomNonRetriable) return NON_RETRIABLE;
|
|
30467
|
+
}
|
|
30468
|
+
if (error.retriable === false) return NON_RETRIABLE;
|
|
30469
|
+
if (error.retriable === true) return RETRIABLE;
|
|
30470
|
+
if (NON_RETRIABLE_ERROR_NAMES.has(error.name)) {
|
|
30471
|
+
return NON_RETRIABLE;
|
|
30472
|
+
}
|
|
30473
|
+
if (error.statusCode && NON_RETRIABLE_STATUS_CODES.has(error.statusCode)) {
|
|
30474
|
+
return NON_RETRIABLE;
|
|
30475
|
+
}
|
|
30476
|
+
if (error.code && RETRIABLE_NETWORK_CODES.has(error.code)) {
|
|
30477
|
+
return RETRIABLE;
|
|
30478
|
+
}
|
|
30479
|
+
if (error.code && RETRIABLE_AWS_CODES.has(error.code)) {
|
|
30480
|
+
return RETRIABLE;
|
|
30481
|
+
}
|
|
30482
|
+
if (error.code && RETRIABLE_AWS_CONFLICTS.has(error.code)) {
|
|
30483
|
+
return RETRIABLE;
|
|
30484
|
+
}
|
|
30485
|
+
if (error.statusCode && RETRIABLE_STATUS_CODES.has(error.statusCode)) {
|
|
30486
|
+
return RETRIABLE;
|
|
30487
|
+
}
|
|
30488
|
+
if (error.message && typeof error.message === "string") {
|
|
30489
|
+
const lowerMessage = error.message.toLowerCase();
|
|
30490
|
+
if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out") || lowerMessage.includes("network") || lowerMessage.includes("connection")) {
|
|
30491
|
+
return RETRIABLE;
|
|
30492
|
+
}
|
|
30493
|
+
}
|
|
30494
|
+
return RETRIABLE;
|
|
30495
|
+
}
|
|
30496
|
+
/**
|
|
30497
|
+
* Check if an error is retriable
|
|
30498
|
+
*
|
|
30499
|
+
* @param {Error} error - The error to check
|
|
30500
|
+
* @param {Object} options - Classification options
|
|
30501
|
+
* @returns {boolean} true if retriable
|
|
30502
|
+
*/
|
|
30503
|
+
static isRetriable(error, options = {}) {
|
|
30504
|
+
return this.classify(error, options) === RETRIABLE;
|
|
30505
|
+
}
|
|
30506
|
+
/**
|
|
30507
|
+
* Check if an error is non-retriable
|
|
30508
|
+
*
|
|
30509
|
+
* @param {Error} error - The error to check
|
|
30510
|
+
* @param {Object} options - Classification options
|
|
30511
|
+
* @returns {boolean} true if non-retriable
|
|
30512
|
+
*/
|
|
30513
|
+
static isNonRetriable(error, options = {}) {
|
|
30514
|
+
return this.classify(error, options) === NON_RETRIABLE;
|
|
27529
30515
|
}
|
|
27530
30516
|
}
|
|
27531
30517
|
|
|
@@ -27546,11 +30532,23 @@ class StateMachinePlugin extends Plugin {
|
|
|
27546
30532
|
workerId: options.workerId || "default",
|
|
27547
30533
|
lockTimeout: options.lockTimeout || 1e3,
|
|
27548
30534
|
// Wait up to 1s for lock
|
|
27549
|
-
lockTTL: options.lockTTL || 5
|
|
30535
|
+
lockTTL: options.lockTTL || 5,
|
|
27550
30536
|
// Lock expires after 5s (prevent deadlock)
|
|
30537
|
+
// Global retry configuration for action execution
|
|
30538
|
+
retryConfig: options.retryConfig || null,
|
|
30539
|
+
// Trigger system configuration
|
|
30540
|
+
enableScheduler: options.enableScheduler || false,
|
|
30541
|
+
schedulerConfig: options.schedulerConfig || {},
|
|
30542
|
+
enableDateTriggers: options.enableDateTriggers !== false,
|
|
30543
|
+
enableFunctionTriggers: options.enableFunctionTriggers !== false,
|
|
30544
|
+
enableEventTriggers: options.enableEventTriggers !== false,
|
|
30545
|
+
triggerCheckInterval: options.triggerCheckInterval || 6e4
|
|
30546
|
+
// Check triggers every 60s by default
|
|
27551
30547
|
};
|
|
27552
30548
|
this.database = null;
|
|
27553
30549
|
this.machines = /* @__PURE__ */ new Map();
|
|
30550
|
+
this.triggerIntervals = [];
|
|
30551
|
+
this.schedulerPlugin = null;
|
|
27554
30552
|
this._validateConfiguration();
|
|
27555
30553
|
}
|
|
27556
30554
|
_validateConfiguration() {
|
|
@@ -27599,7 +30597,8 @@ class StateMachinePlugin extends Plugin {
|
|
|
27599
30597
|
// entityId -> currentState
|
|
27600
30598
|
});
|
|
27601
30599
|
}
|
|
27602
|
-
|
|
30600
|
+
await this._setupTriggers();
|
|
30601
|
+
this.emit("db:plugin:initialized", { machines: Array.from(this.machines.keys()) });
|
|
27603
30602
|
}
|
|
27604
30603
|
async _createStateResources() {
|
|
27605
30604
|
const [logOk] = await tryFn(() => this.database.createResource({
|
|
@@ -27630,6 +30629,8 @@ class StateMachinePlugin extends Plugin {
|
|
|
27630
30629
|
currentState: "string|required",
|
|
27631
30630
|
context: "json|default:{}",
|
|
27632
30631
|
lastTransition: "string|default:null",
|
|
30632
|
+
triggerCounts: "json|default:{}",
|
|
30633
|
+
// Track trigger execution counts
|
|
27633
30634
|
updatedAt: "string|required"
|
|
27634
30635
|
},
|
|
27635
30636
|
behavior: "body-overflow"
|
|
@@ -27693,7 +30694,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
27693
30694
|
if (targetStateConfig && targetStateConfig.entry) {
|
|
27694
30695
|
await this._executeAction(targetStateConfig.entry, context, event, machineId, entityId);
|
|
27695
30696
|
}
|
|
27696
|
-
this.emit("transition", {
|
|
30697
|
+
this.emit("plg:state-machine:transition", {
|
|
27697
30698
|
machineId,
|
|
27698
30699
|
entityId,
|
|
27699
30700
|
from: currentState,
|
|
@@ -27719,14 +30720,97 @@ class StateMachinePlugin extends Plugin {
|
|
|
27719
30720
|
}
|
|
27720
30721
|
return;
|
|
27721
30722
|
}
|
|
27722
|
-
const
|
|
27723
|
-
|
|
27724
|
-
|
|
27725
|
-
|
|
27726
|
-
|
|
27727
|
-
|
|
30723
|
+
const machine = this.machines.get(machineId);
|
|
30724
|
+
const currentState = await this.getState(machineId, entityId);
|
|
30725
|
+
const stateConfig = machine?.config?.states?.[currentState];
|
|
30726
|
+
const retryConfig = {
|
|
30727
|
+
...this.config.retryConfig || {},
|
|
30728
|
+
...machine?.config?.retryConfig || {},
|
|
30729
|
+
...stateConfig?.retryConfig || {}
|
|
30730
|
+
};
|
|
30731
|
+
const maxAttempts = retryConfig.maxAttempts ?? 0;
|
|
30732
|
+
const retryEnabled = maxAttempts > 0;
|
|
30733
|
+
let attempt = 0;
|
|
30734
|
+
while (attempt <= maxAttempts) {
|
|
30735
|
+
try {
|
|
30736
|
+
const result = await action(context, event, { database: this.database, machineId, entityId });
|
|
30737
|
+
if (attempt > 0) {
|
|
30738
|
+
this.emit("plg:state-machine:action-retry-success", {
|
|
30739
|
+
machineId,
|
|
30740
|
+
entityId,
|
|
30741
|
+
action: actionName,
|
|
30742
|
+
attempts: attempt + 1,
|
|
30743
|
+
state: currentState
|
|
30744
|
+
});
|
|
30745
|
+
if (this.config.verbose) {
|
|
30746
|
+
console.log(`[StateMachinePlugin] Action '${actionName}' succeeded after ${attempt + 1} attempts`);
|
|
30747
|
+
}
|
|
30748
|
+
}
|
|
30749
|
+
return result;
|
|
30750
|
+
} catch (error) {
|
|
30751
|
+
if (!retryEnabled) {
|
|
30752
|
+
if (this.config.verbose) {
|
|
30753
|
+
console.error(`[StateMachinePlugin] Action '${actionName}' failed:`, error.message);
|
|
30754
|
+
}
|
|
30755
|
+
this.emit("plg:state-machine:action-error", { actionName, error: error.message, machineId, entityId });
|
|
30756
|
+
return;
|
|
30757
|
+
}
|
|
30758
|
+
const classification = ErrorClassifier.classify(error, {
|
|
30759
|
+
retryableErrors: retryConfig.retryableErrors,
|
|
30760
|
+
nonRetriableErrors: retryConfig.nonRetriableErrors
|
|
30761
|
+
});
|
|
30762
|
+
if (classification === "NON_RETRIABLE") {
|
|
30763
|
+
this.emit("plg:state-machine:action-error-non-retriable", {
|
|
30764
|
+
machineId,
|
|
30765
|
+
entityId,
|
|
30766
|
+
action: actionName,
|
|
30767
|
+
error: error.message,
|
|
30768
|
+
state: currentState
|
|
30769
|
+
});
|
|
30770
|
+
if (this.config.verbose) {
|
|
30771
|
+
console.error(`[StateMachinePlugin] Action '${actionName}' failed with non-retriable error:`, error.message);
|
|
30772
|
+
}
|
|
30773
|
+
throw error;
|
|
30774
|
+
}
|
|
30775
|
+
if (attempt >= maxAttempts) {
|
|
30776
|
+
this.emit("plg:state-machine:action-retry-exhausted", {
|
|
30777
|
+
machineId,
|
|
30778
|
+
entityId,
|
|
30779
|
+
action: actionName,
|
|
30780
|
+
attempts: attempt + 1,
|
|
30781
|
+
error: error.message,
|
|
30782
|
+
state: currentState
|
|
30783
|
+
});
|
|
30784
|
+
if (this.config.verbose) {
|
|
30785
|
+
console.error(`[StateMachinePlugin] Action '${actionName}' failed after ${attempt + 1} attempts:`, error.message);
|
|
30786
|
+
}
|
|
30787
|
+
throw error;
|
|
30788
|
+
}
|
|
30789
|
+
attempt++;
|
|
30790
|
+
const delay = this._calculateBackoff(attempt, retryConfig);
|
|
30791
|
+
if (retryConfig.onRetry) {
|
|
30792
|
+
try {
|
|
30793
|
+
await retryConfig.onRetry(attempt, error, context);
|
|
30794
|
+
} catch (hookError) {
|
|
30795
|
+
if (this.config.verbose) {
|
|
30796
|
+
console.warn(`[StateMachinePlugin] onRetry hook failed:`, hookError.message);
|
|
30797
|
+
}
|
|
30798
|
+
}
|
|
30799
|
+
}
|
|
30800
|
+
this.emit("plg:state-machine:action-retry-attempt", {
|
|
30801
|
+
machineId,
|
|
30802
|
+
entityId,
|
|
30803
|
+
action: actionName,
|
|
30804
|
+
attempt,
|
|
30805
|
+
delay,
|
|
30806
|
+
error: error.message,
|
|
30807
|
+
state: currentState
|
|
30808
|
+
});
|
|
30809
|
+
if (this.config.verbose) {
|
|
30810
|
+
console.warn(`[StateMachinePlugin] Action '${actionName}' failed (attempt ${attempt + 1}/${maxAttempts + 1}), retrying in ${delay}ms:`, error.message);
|
|
30811
|
+
}
|
|
30812
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
27728
30813
|
}
|
|
27729
|
-
this.emit("action_error", { actionName, error: error.message, machineId, entityId });
|
|
27730
30814
|
}
|
|
27731
30815
|
}
|
|
27732
30816
|
async _transition(machineId, entityId, fromState, toState, event, context) {
|
|
@@ -27824,6 +30908,27 @@ class StateMachinePlugin extends Plugin {
|
|
|
27824
30908
|
console.warn(`[StateMachinePlugin] Failed to release lock '${lockName}':`, err.message);
|
|
27825
30909
|
}
|
|
27826
30910
|
}
|
|
30911
|
+
/**
|
|
30912
|
+
* Calculate backoff delay for retry attempts
|
|
30913
|
+
* @private
|
|
30914
|
+
*/
|
|
30915
|
+
_calculateBackoff(attempt, retryConfig) {
|
|
30916
|
+
const {
|
|
30917
|
+
backoffStrategy = "exponential",
|
|
30918
|
+
baseDelay = 1e3,
|
|
30919
|
+
maxDelay = 3e4
|
|
30920
|
+
} = retryConfig || {};
|
|
30921
|
+
let delay;
|
|
30922
|
+
if (backoffStrategy === "exponential") {
|
|
30923
|
+
delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
30924
|
+
} else if (backoffStrategy === "linear") {
|
|
30925
|
+
delay = Math.min(baseDelay * attempt, maxDelay);
|
|
30926
|
+
} else {
|
|
30927
|
+
delay = baseDelay;
|
|
30928
|
+
}
|
|
30929
|
+
const jitter = delay * 0.2 * (Math.random() - 0.5);
|
|
30930
|
+
return Math.round(delay + jitter);
|
|
30931
|
+
}
|
|
27827
30932
|
/**
|
|
27828
30933
|
* Get current state for an entity
|
|
27829
30934
|
*/
|
|
@@ -27953,7 +31058,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
27953
31058
|
if (initialStateConfig && initialStateConfig.entry) {
|
|
27954
31059
|
await this._executeAction(initialStateConfig.entry, context, "INIT", machineId, entityId);
|
|
27955
31060
|
}
|
|
27956
|
-
this.emit("
|
|
31061
|
+
this.emit("plg:state-machine:entity-initialized", { machineId, entityId, initialState });
|
|
27957
31062
|
return initialState;
|
|
27958
31063
|
}
|
|
27959
31064
|
/**
|
|
@@ -28010,12 +31115,343 @@ class StateMachinePlugin extends Plugin {
|
|
|
28010
31115
|
`;
|
|
28011
31116
|
return dot;
|
|
28012
31117
|
}
|
|
31118
|
+
/**
|
|
31119
|
+
* Get all entities currently in a specific state
|
|
31120
|
+
* @private
|
|
31121
|
+
*/
|
|
31122
|
+
async _getEntitiesInState(machineId, stateName) {
|
|
31123
|
+
if (!this.config.persistTransitions) {
|
|
31124
|
+
const machine = this.machines.get(machineId);
|
|
31125
|
+
if (!machine) return [];
|
|
31126
|
+
const entities = [];
|
|
31127
|
+
for (const [entityId, currentState] of machine.currentStates) {
|
|
31128
|
+
if (currentState === stateName) {
|
|
31129
|
+
entities.push({ entityId, currentState, context: {}, triggerCounts: {} });
|
|
31130
|
+
}
|
|
31131
|
+
}
|
|
31132
|
+
return entities;
|
|
31133
|
+
}
|
|
31134
|
+
const [ok, err, records] = await tryFn(
|
|
31135
|
+
() => this.database.resources[this.config.stateResource].query({
|
|
31136
|
+
machineId,
|
|
31137
|
+
currentState: stateName
|
|
31138
|
+
})
|
|
31139
|
+
);
|
|
31140
|
+
if (!ok) {
|
|
31141
|
+
if (this.config.verbose) {
|
|
31142
|
+
console.warn(`[StateMachinePlugin] Failed to query entities in state '${stateName}':`, err.message);
|
|
31143
|
+
}
|
|
31144
|
+
return [];
|
|
31145
|
+
}
|
|
31146
|
+
return records || [];
|
|
31147
|
+
}
|
|
31148
|
+
/**
|
|
31149
|
+
* Increment trigger execution count for an entity
|
|
31150
|
+
* @private
|
|
31151
|
+
*/
|
|
31152
|
+
async _incrementTriggerCount(machineId, entityId, triggerName) {
|
|
31153
|
+
if (!this.config.persistTransitions) {
|
|
31154
|
+
return;
|
|
31155
|
+
}
|
|
31156
|
+
const stateId = `${machineId}_${entityId}`;
|
|
31157
|
+
const [ok, err, stateRecord] = await tryFn(
|
|
31158
|
+
() => this.database.resources[this.config.stateResource].get(stateId)
|
|
31159
|
+
);
|
|
31160
|
+
if (ok && stateRecord) {
|
|
31161
|
+
const triggerCounts = stateRecord.triggerCounts || {};
|
|
31162
|
+
triggerCounts[triggerName] = (triggerCounts[triggerName] || 0) + 1;
|
|
31163
|
+
await tryFn(
|
|
31164
|
+
() => this.database.resources[this.config.stateResource].patch(stateId, { triggerCounts })
|
|
31165
|
+
);
|
|
31166
|
+
}
|
|
31167
|
+
}
|
|
31168
|
+
/**
|
|
31169
|
+
* Setup trigger system for all state machines
|
|
31170
|
+
* @private
|
|
31171
|
+
*/
|
|
31172
|
+
async _setupTriggers() {
|
|
31173
|
+
if (!this.config.enableScheduler && !this.config.enableDateTriggers && !this.config.enableFunctionTriggers && !this.config.enableEventTriggers) {
|
|
31174
|
+
return;
|
|
31175
|
+
}
|
|
31176
|
+
const cronJobs = {};
|
|
31177
|
+
for (const [machineId, machineData] of this.machines) {
|
|
31178
|
+
const machineConfig = machineData.config;
|
|
31179
|
+
for (const [stateName, stateConfig] of Object.entries(machineConfig.states)) {
|
|
31180
|
+
const triggers = stateConfig.triggers || [];
|
|
31181
|
+
for (let i = 0; i < triggers.length; i++) {
|
|
31182
|
+
const trigger = triggers[i];
|
|
31183
|
+
const triggerName = `${trigger.action}_${i}`;
|
|
31184
|
+
if (trigger.type === "cron" && this.config.enableScheduler) {
|
|
31185
|
+
const jobName = `${machineId}_${stateName}_${triggerName}`;
|
|
31186
|
+
cronJobs[jobName] = await this._createCronJob(machineId, stateName, trigger, triggerName);
|
|
31187
|
+
} else if (trigger.type === "date" && this.config.enableDateTriggers) {
|
|
31188
|
+
await this._setupDateTrigger(machineId, stateName, trigger, triggerName);
|
|
31189
|
+
} else if (trigger.type === "function" && this.config.enableFunctionTriggers) {
|
|
31190
|
+
await this._setupFunctionTrigger(machineId, stateName, trigger, triggerName);
|
|
31191
|
+
} else if (trigger.type === "event" && this.config.enableEventTriggers) {
|
|
31192
|
+
await this._setupEventTrigger(machineId, stateName, trigger, triggerName);
|
|
31193
|
+
}
|
|
31194
|
+
}
|
|
31195
|
+
}
|
|
31196
|
+
}
|
|
31197
|
+
if (Object.keys(cronJobs).length > 0 && this.config.enableScheduler) {
|
|
31198
|
+
const { SchedulerPlugin } = await Promise.resolve().then(function () { return scheduler_plugin; });
|
|
31199
|
+
this.schedulerPlugin = new SchedulerPlugin({
|
|
31200
|
+
jobs: cronJobs,
|
|
31201
|
+
persistJobs: false,
|
|
31202
|
+
// Don't persist trigger jobs
|
|
31203
|
+
verbose: this.config.verbose,
|
|
31204
|
+
...this.config.schedulerConfig
|
|
31205
|
+
});
|
|
31206
|
+
await this.database.usePlugin(this.schedulerPlugin);
|
|
31207
|
+
if (this.config.verbose) {
|
|
31208
|
+
console.log(`[StateMachinePlugin] Installed SchedulerPlugin with ${Object.keys(cronJobs).length} cron triggers`);
|
|
31209
|
+
}
|
|
31210
|
+
}
|
|
31211
|
+
}
|
|
31212
|
+
/**
|
|
31213
|
+
* Create a SchedulerPlugin job for a cron trigger
|
|
31214
|
+
* @private
|
|
31215
|
+
*/
|
|
31216
|
+
async _createCronJob(machineId, stateName, trigger, triggerName) {
|
|
31217
|
+
return {
|
|
31218
|
+
schedule: trigger.schedule,
|
|
31219
|
+
description: `Trigger '${triggerName}' for ${machineId}.${stateName}`,
|
|
31220
|
+
action: async (database, context) => {
|
|
31221
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
31222
|
+
let executedCount = 0;
|
|
31223
|
+
for (const entity of entities) {
|
|
31224
|
+
try {
|
|
31225
|
+
if (trigger.condition) {
|
|
31226
|
+
const shouldTrigger = await trigger.condition(entity.context, entity.entityId);
|
|
31227
|
+
if (!shouldTrigger) continue;
|
|
31228
|
+
}
|
|
31229
|
+
if (trigger.maxTriggers !== void 0) {
|
|
31230
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
31231
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
31232
|
+
if (trigger.onMaxTriggersReached) {
|
|
31233
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
31234
|
+
}
|
|
31235
|
+
continue;
|
|
31236
|
+
}
|
|
31237
|
+
}
|
|
31238
|
+
const result = await this._executeAction(
|
|
31239
|
+
trigger.action,
|
|
31240
|
+
entity.context,
|
|
31241
|
+
"TRIGGER",
|
|
31242
|
+
machineId,
|
|
31243
|
+
entity.entityId
|
|
31244
|
+
);
|
|
31245
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
31246
|
+
executedCount++;
|
|
31247
|
+
if (trigger.eventOnSuccess) {
|
|
31248
|
+
await this.send(machineId, entity.entityId, trigger.eventOnSuccess, {
|
|
31249
|
+
...entity.context,
|
|
31250
|
+
triggerResult: result
|
|
31251
|
+
});
|
|
31252
|
+
} else if (trigger.event) {
|
|
31253
|
+
await this.send(machineId, entity.entityId, trigger.event, {
|
|
31254
|
+
...entity.context,
|
|
31255
|
+
triggerResult: result
|
|
31256
|
+
});
|
|
31257
|
+
}
|
|
31258
|
+
this.emit("plg:state-machine:trigger-executed", {
|
|
31259
|
+
machineId,
|
|
31260
|
+
entityId: entity.entityId,
|
|
31261
|
+
state: stateName,
|
|
31262
|
+
trigger: triggerName,
|
|
31263
|
+
type: "cron"
|
|
31264
|
+
});
|
|
31265
|
+
} catch (error) {
|
|
31266
|
+
if (trigger.event) {
|
|
31267
|
+
await tryFn(() => this.send(machineId, entity.entityId, trigger.event, {
|
|
31268
|
+
...entity.context,
|
|
31269
|
+
triggerError: error.message
|
|
31270
|
+
}));
|
|
31271
|
+
}
|
|
31272
|
+
if (this.config.verbose) {
|
|
31273
|
+
console.error(`[StateMachinePlugin] Trigger '${triggerName}' failed for entity ${entity.entityId}:`, error.message);
|
|
31274
|
+
}
|
|
31275
|
+
}
|
|
31276
|
+
}
|
|
31277
|
+
return { processed: entities.length, executed: executedCount };
|
|
31278
|
+
}
|
|
31279
|
+
};
|
|
31280
|
+
}
|
|
31281
|
+
/**
|
|
31282
|
+
* Setup a date-based trigger
|
|
31283
|
+
* @private
|
|
31284
|
+
*/
|
|
31285
|
+
async _setupDateTrigger(machineId, stateName, trigger, triggerName) {
|
|
31286
|
+
const checkInterval = setInterval(async () => {
|
|
31287
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
31288
|
+
for (const entity of entities) {
|
|
31289
|
+
try {
|
|
31290
|
+
const triggerDateValue = entity.context?.[trigger.field];
|
|
31291
|
+
if (!triggerDateValue) continue;
|
|
31292
|
+
const triggerDate = new Date(triggerDateValue);
|
|
31293
|
+
const now = /* @__PURE__ */ new Date();
|
|
31294
|
+
if (now >= triggerDate) {
|
|
31295
|
+
if (trigger.maxTriggers !== void 0) {
|
|
31296
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
31297
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
31298
|
+
if (trigger.onMaxTriggersReached) {
|
|
31299
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
31300
|
+
}
|
|
31301
|
+
continue;
|
|
31302
|
+
}
|
|
31303
|
+
}
|
|
31304
|
+
const result = await this._executeAction(trigger.action, entity.context, "TRIGGER", machineId, entity.entityId);
|
|
31305
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
31306
|
+
if (trigger.event) {
|
|
31307
|
+
await this.send(machineId, entity.entityId, trigger.event, {
|
|
31308
|
+
...entity.context,
|
|
31309
|
+
triggerResult: result
|
|
31310
|
+
});
|
|
31311
|
+
}
|
|
31312
|
+
this.emit("plg:state-machine:trigger-executed", {
|
|
31313
|
+
machineId,
|
|
31314
|
+
entityId: entity.entityId,
|
|
31315
|
+
state: stateName,
|
|
31316
|
+
trigger: triggerName,
|
|
31317
|
+
type: "date"
|
|
31318
|
+
});
|
|
31319
|
+
}
|
|
31320
|
+
} catch (error) {
|
|
31321
|
+
if (this.config.verbose) {
|
|
31322
|
+
console.error(`[StateMachinePlugin] Date trigger '${triggerName}' failed:`, error.message);
|
|
31323
|
+
}
|
|
31324
|
+
}
|
|
31325
|
+
}
|
|
31326
|
+
}, this.config.triggerCheckInterval);
|
|
31327
|
+
this.triggerIntervals.push(checkInterval);
|
|
31328
|
+
}
|
|
31329
|
+
/**
|
|
31330
|
+
* Setup a function-based trigger
|
|
31331
|
+
* @private
|
|
31332
|
+
*/
|
|
31333
|
+
async _setupFunctionTrigger(machineId, stateName, trigger, triggerName) {
|
|
31334
|
+
const interval = trigger.interval || this.config.triggerCheckInterval;
|
|
31335
|
+
const checkInterval = setInterval(async () => {
|
|
31336
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
31337
|
+
for (const entity of entities) {
|
|
31338
|
+
try {
|
|
31339
|
+
if (trigger.maxTriggers !== void 0) {
|
|
31340
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
31341
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
31342
|
+
if (trigger.onMaxTriggersReached) {
|
|
31343
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
31344
|
+
}
|
|
31345
|
+
continue;
|
|
31346
|
+
}
|
|
31347
|
+
}
|
|
31348
|
+
const shouldTrigger = await trigger.condition(entity.context, entity.entityId);
|
|
31349
|
+
if (shouldTrigger) {
|
|
31350
|
+
const result = await this._executeAction(trigger.action, entity.context, "TRIGGER", machineId, entity.entityId);
|
|
31351
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
31352
|
+
if (trigger.event) {
|
|
31353
|
+
await this.send(machineId, entity.entityId, trigger.event, {
|
|
31354
|
+
...entity.context,
|
|
31355
|
+
triggerResult: result
|
|
31356
|
+
});
|
|
31357
|
+
}
|
|
31358
|
+
this.emit("plg:state-machine:trigger-executed", {
|
|
31359
|
+
machineId,
|
|
31360
|
+
entityId: entity.entityId,
|
|
31361
|
+
state: stateName,
|
|
31362
|
+
trigger: triggerName,
|
|
31363
|
+
type: "function"
|
|
31364
|
+
});
|
|
31365
|
+
}
|
|
31366
|
+
} catch (error) {
|
|
31367
|
+
if (this.config.verbose) {
|
|
31368
|
+
console.error(`[StateMachinePlugin] Function trigger '${triggerName}' failed:`, error.message);
|
|
31369
|
+
}
|
|
31370
|
+
}
|
|
31371
|
+
}
|
|
31372
|
+
}, interval);
|
|
31373
|
+
this.triggerIntervals.push(checkInterval);
|
|
31374
|
+
}
|
|
31375
|
+
/**
|
|
31376
|
+
* Setup an event-based trigger
|
|
31377
|
+
* @private
|
|
31378
|
+
*/
|
|
31379
|
+
async _setupEventTrigger(machineId, stateName, trigger, triggerName) {
|
|
31380
|
+
const eventName = trigger.event;
|
|
31381
|
+
const eventHandler = async (eventData) => {
|
|
31382
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
31383
|
+
for (const entity of entities) {
|
|
31384
|
+
try {
|
|
31385
|
+
if (trigger.condition) {
|
|
31386
|
+
const shouldTrigger = await trigger.condition(entity.context, entity.entityId, eventData);
|
|
31387
|
+
if (!shouldTrigger) continue;
|
|
31388
|
+
}
|
|
31389
|
+
if (trigger.maxTriggers !== void 0) {
|
|
31390
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
31391
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
31392
|
+
if (trigger.onMaxTriggersReached) {
|
|
31393
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
31394
|
+
}
|
|
31395
|
+
continue;
|
|
31396
|
+
}
|
|
31397
|
+
}
|
|
31398
|
+
const result = await this._executeAction(
|
|
31399
|
+
trigger.action,
|
|
31400
|
+
{ ...entity.context, eventData },
|
|
31401
|
+
"TRIGGER",
|
|
31402
|
+
machineId,
|
|
31403
|
+
entity.entityId
|
|
31404
|
+
);
|
|
31405
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
31406
|
+
if (trigger.sendEvent) {
|
|
31407
|
+
await this.send(machineId, entity.entityId, trigger.sendEvent, {
|
|
31408
|
+
...entity.context,
|
|
31409
|
+
triggerResult: result,
|
|
31410
|
+
eventData
|
|
31411
|
+
});
|
|
31412
|
+
}
|
|
31413
|
+
this.emit("plg:state-machine:trigger-executed", {
|
|
31414
|
+
machineId,
|
|
31415
|
+
entityId: entity.entityId,
|
|
31416
|
+
state: stateName,
|
|
31417
|
+
trigger: triggerName,
|
|
31418
|
+
type: "event",
|
|
31419
|
+
eventName
|
|
31420
|
+
});
|
|
31421
|
+
} catch (error) {
|
|
31422
|
+
if (this.config.verbose) {
|
|
31423
|
+
console.error(`[StateMachinePlugin] Event trigger '${triggerName}' failed:`, error.message);
|
|
31424
|
+
}
|
|
31425
|
+
}
|
|
31426
|
+
}
|
|
31427
|
+
};
|
|
31428
|
+
if (eventName.startsWith("db:")) {
|
|
31429
|
+
const dbEventName = eventName.substring(3);
|
|
31430
|
+
this.database.on(dbEventName, eventHandler);
|
|
31431
|
+
if (this.config.verbose) {
|
|
31432
|
+
console.log(`[StateMachinePlugin] Listening to database event '${dbEventName}' for trigger '${triggerName}'`);
|
|
31433
|
+
}
|
|
31434
|
+
} else {
|
|
31435
|
+
this.on(eventName, eventHandler);
|
|
31436
|
+
if (this.config.verbose) {
|
|
31437
|
+
console.log(`[StateMachinePlugin] Listening to plugin event '${eventName}' for trigger '${triggerName}'`);
|
|
31438
|
+
}
|
|
31439
|
+
}
|
|
31440
|
+
}
|
|
28013
31441
|
async start() {
|
|
28014
31442
|
if (this.config.verbose) {
|
|
28015
31443
|
console.log(`[StateMachinePlugin] Started with ${this.machines.size} state machines`);
|
|
28016
31444
|
}
|
|
28017
31445
|
}
|
|
28018
31446
|
async stop() {
|
|
31447
|
+
for (const interval of this.triggerIntervals) {
|
|
31448
|
+
clearInterval(interval);
|
|
31449
|
+
}
|
|
31450
|
+
this.triggerIntervals = [];
|
|
31451
|
+
if (this.schedulerPlugin) {
|
|
31452
|
+
await this.schedulerPlugin.stop();
|
|
31453
|
+
this.schedulerPlugin = null;
|
|
31454
|
+
}
|
|
28019
31455
|
this.machines.clear();
|
|
28020
31456
|
this.removeAllListeners();
|
|
28021
31457
|
}
|
|
@@ -38157,11 +41593,21 @@ class TfStatePlugin extends Plugin {
|
|
|
38157
41593
|
}
|
|
38158
41594
|
}
|
|
38159
41595
|
|
|
41596
|
+
const ONE_HOUR_SEC = 3600;
|
|
41597
|
+
const ONE_DAY_SEC = 86400;
|
|
41598
|
+
const THIRTY_DAYS_SEC = 2592e3;
|
|
41599
|
+
const TEN_SECONDS_MS = 1e4;
|
|
41600
|
+
const ONE_MINUTE_MS = 6e4;
|
|
41601
|
+
const TEN_MINUTES_MS = 6e5;
|
|
41602
|
+
const ONE_HOUR_MS = 36e5;
|
|
41603
|
+
const ONE_DAY_MS = 864e5;
|
|
41604
|
+
const ONE_WEEK_MS = 6048e5;
|
|
41605
|
+
const SECONDS_TO_MS = 1e3;
|
|
38160
41606
|
const GRANULARITIES = {
|
|
38161
41607
|
minute: {
|
|
38162
|
-
threshold:
|
|
41608
|
+
threshold: ONE_HOUR_SEC,
|
|
38163
41609
|
// TTL < 1 hour
|
|
38164
|
-
interval:
|
|
41610
|
+
interval: TEN_SECONDS_MS,
|
|
38165
41611
|
// Check every 10 seconds
|
|
38166
41612
|
cohortsToCheck: 3,
|
|
38167
41613
|
// Check last 3 minutes
|
|
@@ -38169,9 +41615,9 @@ const GRANULARITIES = {
|
|
|
38169
41615
|
// '2024-10-25T14:30'
|
|
38170
41616
|
},
|
|
38171
41617
|
hour: {
|
|
38172
|
-
threshold:
|
|
41618
|
+
threshold: ONE_DAY_SEC,
|
|
38173
41619
|
// TTL < 24 hours
|
|
38174
|
-
interval:
|
|
41620
|
+
interval: TEN_MINUTES_MS,
|
|
38175
41621
|
// Check every 10 minutes
|
|
38176
41622
|
cohortsToCheck: 2,
|
|
38177
41623
|
// Check last 2 hours
|
|
@@ -38179,9 +41625,9 @@ const GRANULARITIES = {
|
|
|
38179
41625
|
// '2024-10-25T14'
|
|
38180
41626
|
},
|
|
38181
41627
|
day: {
|
|
38182
|
-
threshold:
|
|
41628
|
+
threshold: THIRTY_DAYS_SEC,
|
|
38183
41629
|
// TTL < 30 days
|
|
38184
|
-
interval:
|
|
41630
|
+
interval: ONE_HOUR_MS,
|
|
38185
41631
|
// Check every 1 hour
|
|
38186
41632
|
cohortsToCheck: 2,
|
|
38187
41633
|
// Check last 2 days
|
|
@@ -38191,7 +41637,7 @@ const GRANULARITIES = {
|
|
|
38191
41637
|
week: {
|
|
38192
41638
|
threshold: Infinity,
|
|
38193
41639
|
// TTL >= 30 days
|
|
38194
|
-
interval:
|
|
41640
|
+
interval: ONE_DAY_MS,
|
|
38195
41641
|
// Check every 24 hours
|
|
38196
41642
|
cohortsToCheck: 2,
|
|
38197
41643
|
// Check last 2 weeks
|
|
@@ -38207,7 +41653,7 @@ function getWeekNumber(date) {
|
|
|
38207
41653
|
const dayNum = d.getUTCDay() || 7;
|
|
38208
41654
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
38209
41655
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
38210
|
-
return Math.ceil(((d - yearStart) /
|
|
41656
|
+
return Math.ceil(((d - yearStart) / ONE_DAY_MS + 1) / 7);
|
|
38211
41657
|
}
|
|
38212
41658
|
function detectGranularity(ttl) {
|
|
38213
41659
|
if (!ttl) return "day";
|
|
@@ -38224,16 +41670,16 @@ function getExpiredCohorts(granularity, count) {
|
|
|
38224
41670
|
let checkDate;
|
|
38225
41671
|
switch (granularity) {
|
|
38226
41672
|
case "minute":
|
|
38227
|
-
checkDate = new Date(now.getTime() - i *
|
|
41673
|
+
checkDate = new Date(now.getTime() - i * ONE_MINUTE_MS);
|
|
38228
41674
|
break;
|
|
38229
41675
|
case "hour":
|
|
38230
|
-
checkDate = new Date(now.getTime() - i *
|
|
41676
|
+
checkDate = new Date(now.getTime() - i * ONE_HOUR_MS);
|
|
38231
41677
|
break;
|
|
38232
41678
|
case "day":
|
|
38233
|
-
checkDate = new Date(now.getTime() - i *
|
|
41679
|
+
checkDate = new Date(now.getTime() - i * ONE_DAY_MS);
|
|
38234
41680
|
break;
|
|
38235
41681
|
case "week":
|
|
38236
|
-
checkDate = new Date(now.getTime() - i *
|
|
41682
|
+
checkDate = new Date(now.getTime() - i * ONE_WEEK_MS);
|
|
38237
41683
|
break;
|
|
38238
41684
|
}
|
|
38239
41685
|
cohorts.push(config.cohortFormat(checkDate));
|
|
@@ -38277,7 +41723,7 @@ class TTLPlugin extends Plugin {
|
|
|
38277
41723
|
if (this.verbose) {
|
|
38278
41724
|
console.log(`[TTLPlugin] Installed with ${Object.keys(this.resources).length} resources`);
|
|
38279
41725
|
}
|
|
38280
|
-
this.emit("installed", {
|
|
41726
|
+
this.emit("db:plugin:installed", {
|
|
38281
41727
|
plugin: "TTLPlugin",
|
|
38282
41728
|
resources: Object.keys(this.resources)
|
|
38283
41729
|
});
|
|
@@ -38399,7 +41845,7 @@ class TTLPlugin extends Plugin {
|
|
|
38399
41845
|
return;
|
|
38400
41846
|
}
|
|
38401
41847
|
const baseTimestamp = typeof baseTime === "number" ? baseTime : new Date(baseTime).getTime();
|
|
38402
|
-
const expiresAt = config.ttl ? new Date(baseTimestamp + config.ttl *
|
|
41848
|
+
const expiresAt = config.ttl ? new Date(baseTimestamp + config.ttl * SECONDS_TO_MS) : new Date(baseTimestamp);
|
|
38403
41849
|
const cohortConfig = GRANULARITIES[config.granularity];
|
|
38404
41850
|
const cohort = cohortConfig.cohortFormat(expiresAt);
|
|
38405
41851
|
const indexId = `${resourceName}:${record.id}`;
|
|
@@ -38514,7 +41960,7 @@ class TTLPlugin extends Plugin {
|
|
|
38514
41960
|
}
|
|
38515
41961
|
this.stats.lastScanAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38516
41962
|
this.stats.lastScanDuration = Date.now() - startTime;
|
|
38517
|
-
this.emit("
|
|
41963
|
+
this.emit("plg:ttl:scan-completed", {
|
|
38518
41964
|
granularity,
|
|
38519
41965
|
duration: this.stats.lastScanDuration,
|
|
38520
41966
|
cohorts
|
|
@@ -38522,7 +41968,7 @@ class TTLPlugin extends Plugin {
|
|
|
38522
41968
|
} catch (error) {
|
|
38523
41969
|
console.error(`[TTLPlugin] Error in ${granularity} cleanup:`, error);
|
|
38524
41970
|
this.stats.totalErrors++;
|
|
38525
|
-
this.emit("
|
|
41971
|
+
this.emit("plg:ttl:cleanup-error", { granularity, error });
|
|
38526
41972
|
}
|
|
38527
41973
|
}
|
|
38528
41974
|
/**
|
|
@@ -38570,7 +42016,7 @@ class TTLPlugin extends Plugin {
|
|
|
38570
42016
|
}
|
|
38571
42017
|
await this.expirationIndex.delete(entry.id);
|
|
38572
42018
|
this.stats.totalExpired++;
|
|
38573
|
-
this.emit("
|
|
42019
|
+
this.emit("plg:ttl:record-expired", { resource: entry.resourceName, record });
|
|
38574
42020
|
} catch (error) {
|
|
38575
42021
|
console.error(`[TTLPlugin] Error processing expired entry:`, error);
|
|
38576
42022
|
this.stats.totalErrors++;
|
|
@@ -39041,15 +42487,15 @@ class VectorPlugin extends Plugin {
|
|
|
39041
42487
|
this._throttleState = /* @__PURE__ */ new Map();
|
|
39042
42488
|
}
|
|
39043
42489
|
async onInstall() {
|
|
39044
|
-
this.emit("installed", { plugin: "VectorPlugin" });
|
|
42490
|
+
this.emit("db:plugin:installed", { plugin: "VectorPlugin" });
|
|
39045
42491
|
this.validateVectorStorage();
|
|
39046
42492
|
this.installResourceMethods();
|
|
39047
42493
|
}
|
|
39048
42494
|
async onStart() {
|
|
39049
|
-
this.emit("started", { plugin: "VectorPlugin" });
|
|
42495
|
+
this.emit("db:plugin:started", { plugin: "VectorPlugin" });
|
|
39050
42496
|
}
|
|
39051
42497
|
async onStop() {
|
|
39052
|
-
this.emit("stopped", { plugin: "VectorPlugin" });
|
|
42498
|
+
this.emit("db:plugin:stopped", { plugin: "VectorPlugin" });
|
|
39053
42499
|
}
|
|
39054
42500
|
async onUninstall(options) {
|
|
39055
42501
|
for (const resource of Object.values(this.database.resources)) {
|
|
@@ -39060,7 +42506,7 @@ class VectorPlugin extends Plugin {
|
|
|
39060
42506
|
delete resource.findSimilar;
|
|
39061
42507
|
delete resource.distance;
|
|
39062
42508
|
}
|
|
39063
|
-
this.emit("uninstalled", { plugin: "VectorPlugin" });
|
|
42509
|
+
this.emit("db:plugin:uninstalled", { plugin: "VectorPlugin" });
|
|
39064
42510
|
}
|
|
39065
42511
|
/**
|
|
39066
42512
|
* Validate vector storage configuration for all resources
|
|
@@ -39089,10 +42535,10 @@ class VectorPlugin extends Plugin {
|
|
|
39089
42535
|
currentBehavior: resource.behavior || "default",
|
|
39090
42536
|
recommendation: "body-overflow"
|
|
39091
42537
|
};
|
|
39092
|
-
this.emit("vector:storage-warning", warning);
|
|
42538
|
+
this.emit("plg:vector:storage-warning", warning);
|
|
39093
42539
|
if (this.config.autoFixBehavior) {
|
|
39094
42540
|
resource.behavior = "body-overflow";
|
|
39095
|
-
this.emit("vector:behavior-fixed", {
|
|
42541
|
+
this.emit("plg:vector:behavior-fixed", {
|
|
39096
42542
|
resource: resource.name,
|
|
39097
42543
|
newBehavior: "body-overflow"
|
|
39098
42544
|
});
|
|
@@ -39124,7 +42570,7 @@ class VectorPlugin extends Plugin {
|
|
|
39124
42570
|
const partitionName = `byHas${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
|
|
39125
42571
|
const trackingFieldName = `_has${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
|
|
39126
42572
|
if (resource.config.partitions && resource.config.partitions[partitionName]) {
|
|
39127
|
-
this.emit("vector:partition-exists", {
|
|
42573
|
+
this.emit("plg:vector:partition-exists", {
|
|
39128
42574
|
resource: resource.name,
|
|
39129
42575
|
vectorField: vectorField.name,
|
|
39130
42576
|
partition: partitionName,
|
|
@@ -39147,7 +42593,7 @@ class VectorPlugin extends Plugin {
|
|
|
39147
42593
|
default: false
|
|
39148
42594
|
}, "VectorPlugin");
|
|
39149
42595
|
}
|
|
39150
|
-
this.emit("vector:partition-created", {
|
|
42596
|
+
this.emit("plg:vector:partition-created", {
|
|
39151
42597
|
resource: resource.name,
|
|
39152
42598
|
vectorField: vectorField.name,
|
|
39153
42599
|
partition: partitionName,
|
|
@@ -39222,7 +42668,7 @@ class VectorPlugin extends Plugin {
|
|
|
39222
42668
|
}
|
|
39223
42669
|
return updates;
|
|
39224
42670
|
});
|
|
39225
|
-
this.emit("vector:hooks-installed", {
|
|
42671
|
+
this.emit("plg:vector:hooks-installed", {
|
|
39226
42672
|
resource: resource.name,
|
|
39227
42673
|
vectorField,
|
|
39228
42674
|
trackingField,
|
|
@@ -39331,7 +42777,7 @@ class VectorPlugin extends Plugin {
|
|
|
39331
42777
|
const vectorField = this._findEmbeddingField(resource.schema.attributes);
|
|
39332
42778
|
this._vectorFieldCache.set(resource.name, vectorField);
|
|
39333
42779
|
if (vectorField && this.config.emitEvents) {
|
|
39334
|
-
this.emit("vector:field-detected", {
|
|
42780
|
+
this.emit("plg:vector:field-detected", {
|
|
39335
42781
|
resource: resource.name,
|
|
39336
42782
|
vectorField,
|
|
39337
42783
|
timestamp: Date.now()
|
|
@@ -40255,7 +43701,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40255
43701
|
async sendCommand(command) {
|
|
40256
43702
|
const commandName = command.constructor.name;
|
|
40257
43703
|
const input = command.input || {};
|
|
40258
|
-
this.emit("
|
|
43704
|
+
this.emit("cl:request", commandName, input);
|
|
40259
43705
|
let response;
|
|
40260
43706
|
try {
|
|
40261
43707
|
switch (commandName) {
|
|
@@ -40283,7 +43729,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40283
43729
|
default:
|
|
40284
43730
|
throw new Error(`Unsupported command: ${commandName}`);
|
|
40285
43731
|
}
|
|
40286
|
-
this.emit("
|
|
43732
|
+
this.emit("cl:response", commandName, response, input);
|
|
40287
43733
|
return response;
|
|
40288
43734
|
} catch (error) {
|
|
40289
43735
|
const mappedError = mapAwsError(error, {
|
|
@@ -40394,7 +43840,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40394
43840
|
contentLength,
|
|
40395
43841
|
ifMatch
|
|
40396
43842
|
});
|
|
40397
|
-
this.emit("
|
|
43843
|
+
this.emit("cl:PutObject", null, { key, metadata, contentType, body, contentEncoding, contentLength });
|
|
40398
43844
|
return response;
|
|
40399
43845
|
}
|
|
40400
43846
|
/**
|
|
@@ -40409,7 +43855,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40409
43855
|
decodedMetadata[k] = metadataDecode(v);
|
|
40410
43856
|
}
|
|
40411
43857
|
}
|
|
40412
|
-
this.emit("
|
|
43858
|
+
this.emit("cl:GetObject", null, { key });
|
|
40413
43859
|
return {
|
|
40414
43860
|
...response,
|
|
40415
43861
|
Metadata: decodedMetadata
|
|
@@ -40427,7 +43873,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40427
43873
|
decodedMetadata[k] = metadataDecode(v);
|
|
40428
43874
|
}
|
|
40429
43875
|
}
|
|
40430
|
-
this.emit("
|
|
43876
|
+
this.emit("cl:HeadObject", null, { key });
|
|
40431
43877
|
return {
|
|
40432
43878
|
...response,
|
|
40433
43879
|
Metadata: decodedMetadata
|
|
@@ -40452,7 +43898,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40452
43898
|
metadataDirective,
|
|
40453
43899
|
contentType
|
|
40454
43900
|
});
|
|
40455
|
-
this.emit("
|
|
43901
|
+
this.emit("cl:CopyObject", null, { from, to, metadata, metadataDirective });
|
|
40456
43902
|
return response;
|
|
40457
43903
|
}
|
|
40458
43904
|
/**
|
|
@@ -40468,7 +43914,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40468
43914
|
async deleteObject(key) {
|
|
40469
43915
|
const fullKey = this.keyPrefix ? path$1.join(this.keyPrefix, key) : key;
|
|
40470
43916
|
const response = await this.storage.delete(fullKey);
|
|
40471
|
-
this.emit("
|
|
43917
|
+
this.emit("cl:DeleteObject", null, { key });
|
|
40472
43918
|
return response;
|
|
40473
43919
|
}
|
|
40474
43920
|
/**
|
|
@@ -40501,7 +43947,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40501
43947
|
maxKeys,
|
|
40502
43948
|
continuationToken
|
|
40503
43949
|
});
|
|
40504
|
-
this.emit("
|
|
43950
|
+
this.emit("cl:ListObjects", null, { prefix, count: response.Contents.length });
|
|
40505
43951
|
return response;
|
|
40506
43952
|
}
|
|
40507
43953
|
/**
|
|
@@ -40541,7 +43987,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40541
43987
|
if (this.keyPrefix) {
|
|
40542
43988
|
keys = keys.map((x) => x.replace(this.keyPrefix, "")).map((x) => x.startsWith("/") ? x.replace("/", "") : x);
|
|
40543
43989
|
}
|
|
40544
|
-
this.emit("
|
|
43990
|
+
this.emit("cl:GetKeysPage", keys, params);
|
|
40545
43991
|
return keys;
|
|
40546
43992
|
}
|
|
40547
43993
|
/**
|
|
@@ -40558,7 +44004,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40558
44004
|
if (this.keyPrefix) {
|
|
40559
44005
|
keys = keys.map((x) => x.replace(this.keyPrefix, "")).map((x) => x.startsWith("/") ? x.replace("/", "") : x);
|
|
40560
44006
|
}
|
|
40561
|
-
this.emit("
|
|
44007
|
+
this.emit("cl:GetAllKeys", keys, { prefix });
|
|
40562
44008
|
return keys;
|
|
40563
44009
|
}
|
|
40564
44010
|
/**
|
|
@@ -40567,7 +44013,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40567
44013
|
async count({ prefix = "" } = {}) {
|
|
40568
44014
|
const keys = await this.getAllKeys({ prefix });
|
|
40569
44015
|
const count = keys.length;
|
|
40570
|
-
this.emit("
|
|
44016
|
+
this.emit("cl:Count", count, { prefix });
|
|
40571
44017
|
return count;
|
|
40572
44018
|
}
|
|
40573
44019
|
/**
|
|
@@ -40579,13 +44025,13 @@ class MemoryClient extends EventEmitter {
|
|
|
40579
44025
|
if (keys.length > 0) {
|
|
40580
44026
|
const result = await this.deleteObjects(keys);
|
|
40581
44027
|
totalDeleted = result.Deleted.length;
|
|
40582
|
-
this.emit("
|
|
44028
|
+
this.emit("cl:DeleteAll", {
|
|
40583
44029
|
prefix,
|
|
40584
44030
|
batch: totalDeleted,
|
|
40585
44031
|
total: totalDeleted
|
|
40586
44032
|
});
|
|
40587
44033
|
}
|
|
40588
|
-
this.emit("
|
|
44034
|
+
this.emit("cl:DeleteAllComplete", {
|
|
40589
44035
|
prefix,
|
|
40590
44036
|
totalDeleted
|
|
40591
44037
|
});
|
|
@@ -40598,11 +44044,11 @@ class MemoryClient extends EventEmitter {
|
|
|
40598
44044
|
if (offset === 0) return null;
|
|
40599
44045
|
const keys = await this.getAllKeys({ prefix });
|
|
40600
44046
|
if (offset >= keys.length) {
|
|
40601
|
-
this.emit("
|
|
44047
|
+
this.emit("cl:GetContinuationTokenAfterOffset", null, { prefix, offset });
|
|
40602
44048
|
return null;
|
|
40603
44049
|
}
|
|
40604
44050
|
const token = keys[offset];
|
|
40605
|
-
this.emit("
|
|
44051
|
+
this.emit("cl:GetContinuationTokenAfterOffset", token, { prefix, offset });
|
|
40606
44052
|
return token;
|
|
40607
44053
|
}
|
|
40608
44054
|
/**
|
|
@@ -40632,7 +44078,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40632
44078
|
});
|
|
40633
44079
|
}
|
|
40634
44080
|
}
|
|
40635
|
-
this.emit("
|
|
44081
|
+
this.emit("cl:MoveAllObjects", { results, errors }, { prefixFrom, prefixTo });
|
|
40636
44082
|
if (errors.length > 0) {
|
|
40637
44083
|
const error = new Error("Some objects could not be moved");
|
|
40638
44084
|
error.context = {
|