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.cjs.js
CHANGED
|
@@ -2567,6 +2567,18 @@ const PLUGIN_DEPENDENCIES = {
|
|
|
2567
2567
|
npmUrl: "https://www.npmjs.com/package/@hono/swagger-ui"
|
|
2568
2568
|
}
|
|
2569
2569
|
}
|
|
2570
|
+
},
|
|
2571
|
+
"ml-plugin": {
|
|
2572
|
+
name: "ML Plugin",
|
|
2573
|
+
docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/ml-plugin.md",
|
|
2574
|
+
dependencies: {
|
|
2575
|
+
"@tensorflow/tfjs-node": {
|
|
2576
|
+
version: "^4.0.0",
|
|
2577
|
+
description: "TensorFlow.js for Node.js with native bindings",
|
|
2578
|
+
installCommand: "pnpm add @tensorflow/tfjs-node",
|
|
2579
|
+
npmUrl: "https://www.npmjs.com/package/@tensorflow/tfjs-node"
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2570
2582
|
}
|
|
2571
2583
|
};
|
|
2572
2584
|
function isVersionCompatible(actual, required) {
|
|
@@ -2703,6 +2715,2037 @@ async function requirePluginDependency(pluginId, options = {}) {
|
|
|
2703
2715
|
return { valid, missing, incompatible, messages };
|
|
2704
2716
|
}
|
|
2705
2717
|
|
|
2718
|
+
function success(data, options = {}) {
|
|
2719
|
+
const { status = 200, meta = {} } = options;
|
|
2720
|
+
return {
|
|
2721
|
+
success: true,
|
|
2722
|
+
data,
|
|
2723
|
+
meta: {
|
|
2724
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2725
|
+
...meta
|
|
2726
|
+
},
|
|
2727
|
+
_status: status
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
function error(error2, options = {}) {
|
|
2731
|
+
const { status = 500, code = "INTERNAL_ERROR", details = {} } = options;
|
|
2732
|
+
const errorMessage = error2 instanceof Error ? error2.message : error2;
|
|
2733
|
+
const errorStack = error2 instanceof Error && process.env.NODE_ENV !== "production" ? error2.stack : void 0;
|
|
2734
|
+
return {
|
|
2735
|
+
success: false,
|
|
2736
|
+
error: {
|
|
2737
|
+
message: errorMessage,
|
|
2738
|
+
code,
|
|
2739
|
+
details,
|
|
2740
|
+
stack: errorStack
|
|
2741
|
+
},
|
|
2742
|
+
meta: {
|
|
2743
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2744
|
+
},
|
|
2745
|
+
_status: status
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
function list(items, pagination = {}) {
|
|
2749
|
+
const { total, page, pageSize, pageCount } = pagination;
|
|
2750
|
+
return {
|
|
2751
|
+
success: true,
|
|
2752
|
+
data: items,
|
|
2753
|
+
pagination: {
|
|
2754
|
+
total: total || items.length,
|
|
2755
|
+
page: page || 1,
|
|
2756
|
+
pageSize: pageSize || items.length,
|
|
2757
|
+
pageCount: pageCount || 1
|
|
2758
|
+
},
|
|
2759
|
+
meta: {
|
|
2760
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2761
|
+
},
|
|
2762
|
+
_status: 200
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
function created(data, location) {
|
|
2766
|
+
return {
|
|
2767
|
+
success: true,
|
|
2768
|
+
data,
|
|
2769
|
+
meta: {
|
|
2770
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2771
|
+
location
|
|
2772
|
+
},
|
|
2773
|
+
_status: 201
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
function noContent() {
|
|
2777
|
+
return {
|
|
2778
|
+
success: true,
|
|
2779
|
+
data: null,
|
|
2780
|
+
meta: {
|
|
2781
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2782
|
+
},
|
|
2783
|
+
_status: 204
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
function notFound(resource, id) {
|
|
2787
|
+
return error(`${resource} with id '${id}' not found`, {
|
|
2788
|
+
status: 404,
|
|
2789
|
+
code: "NOT_FOUND",
|
|
2790
|
+
details: { resource, id }
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
function payloadTooLarge(size, limit) {
|
|
2794
|
+
return error("Request payload too large", {
|
|
2795
|
+
status: 413,
|
|
2796
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
2797
|
+
details: {
|
|
2798
|
+
receivedSize: size,
|
|
2799
|
+
maxSize: limit,
|
|
2800
|
+
receivedMB: (size / 1024 / 1024).toFixed(2),
|
|
2801
|
+
maxMB: (limit / 1024 / 1024).toFixed(2)
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
const errorStatusMap = {
|
|
2807
|
+
"ValidationError": 400,
|
|
2808
|
+
"InvalidResourceItem": 400,
|
|
2809
|
+
"ResourceNotFound": 404,
|
|
2810
|
+
"NoSuchKey": 404,
|
|
2811
|
+
"NoSuchBucket": 404,
|
|
2812
|
+
"PartitionError": 400,
|
|
2813
|
+
"CryptoError": 500,
|
|
2814
|
+
"SchemaError": 400,
|
|
2815
|
+
"QueueError": 500,
|
|
2816
|
+
"ResourceError": 500
|
|
2817
|
+
};
|
|
2818
|
+
function getStatusFromError(err) {
|
|
2819
|
+
if (err.name && errorStatusMap[err.name]) {
|
|
2820
|
+
return errorStatusMap[err.name];
|
|
2821
|
+
}
|
|
2822
|
+
if (err.constructor && err.constructor.name && errorStatusMap[err.constructor.name]) {
|
|
2823
|
+
return errorStatusMap[err.constructor.name];
|
|
2824
|
+
}
|
|
2825
|
+
if (err.message) {
|
|
2826
|
+
if (err.message.includes("not found") || err.message.includes("does not exist")) {
|
|
2827
|
+
return 404;
|
|
2828
|
+
}
|
|
2829
|
+
if (err.message.includes("validation") || err.message.includes("invalid")) {
|
|
2830
|
+
return 400;
|
|
2831
|
+
}
|
|
2832
|
+
if (err.message.includes("unauthorized") || err.message.includes("authentication")) {
|
|
2833
|
+
return 401;
|
|
2834
|
+
}
|
|
2835
|
+
if (err.message.includes("forbidden") || err.message.includes("permission")) {
|
|
2836
|
+
return 403;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return 500;
|
|
2840
|
+
}
|
|
2841
|
+
function errorHandler(err, c) {
|
|
2842
|
+
const status = getStatusFromError(err);
|
|
2843
|
+
const code = err.name || "INTERNAL_ERROR";
|
|
2844
|
+
const details = {};
|
|
2845
|
+
if (err.resource) details.resource = err.resource;
|
|
2846
|
+
if (err.bucket) details.bucket = err.bucket;
|
|
2847
|
+
if (err.key) details.key = err.key;
|
|
2848
|
+
if (err.operation) details.operation = err.operation;
|
|
2849
|
+
if (err.suggestion) details.suggestion = err.suggestion;
|
|
2850
|
+
if (err.availableResources) details.availableResources = err.availableResources;
|
|
2851
|
+
const response = error(err, {
|
|
2852
|
+
status,
|
|
2853
|
+
code,
|
|
2854
|
+
details
|
|
2855
|
+
});
|
|
2856
|
+
if (status >= 500) {
|
|
2857
|
+
console.error("[API Plugin] Error:", {
|
|
2858
|
+
message: err.message,
|
|
2859
|
+
code,
|
|
2860
|
+
status,
|
|
2861
|
+
stack: err.stack,
|
|
2862
|
+
details
|
|
2863
|
+
});
|
|
2864
|
+
} else if (status >= 400 && status < 500 && c.get("verbose")) {
|
|
2865
|
+
console.warn("[API Plugin] Client error:", {
|
|
2866
|
+
message: err.message,
|
|
2867
|
+
code,
|
|
2868
|
+
status,
|
|
2869
|
+
details
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
return c.json(response, response._status);
|
|
2873
|
+
}
|
|
2874
|
+
function asyncHandler(fn) {
|
|
2875
|
+
return async (c) => {
|
|
2876
|
+
try {
|
|
2877
|
+
return await fn(c);
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
return errorHandler(err, c);
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
function parseCustomRoute(routeDef) {
|
|
2885
|
+
let def = routeDef.trim();
|
|
2886
|
+
const isAsync = def.startsWith("async ");
|
|
2887
|
+
if (isAsync) {
|
|
2888
|
+
def = def.substring(6).trim();
|
|
2889
|
+
}
|
|
2890
|
+
const parts = def.split(/\s+/);
|
|
2891
|
+
if (parts.length < 2) {
|
|
2892
|
+
throw new Error(`Invalid route definition: "${routeDef}". Expected format: "METHOD /path" or "async METHOD /path"`);
|
|
2893
|
+
}
|
|
2894
|
+
const method = parts[0].toUpperCase();
|
|
2895
|
+
const path = parts.slice(1).join(" ").trim();
|
|
2896
|
+
const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
2897
|
+
if (!validMethods.includes(method)) {
|
|
2898
|
+
throw new Error(`Invalid HTTP method: "${method}". Must be one of: ${validMethods.join(", ")}`);
|
|
2899
|
+
}
|
|
2900
|
+
if (!path.startsWith("/")) {
|
|
2901
|
+
throw new Error(`Invalid route path: "${path}". Path must start with "/"`);
|
|
2902
|
+
}
|
|
2903
|
+
return { method, path, isAsync };
|
|
2904
|
+
}
|
|
2905
|
+
function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
2906
|
+
const app = new Hono();
|
|
2907
|
+
const {
|
|
2908
|
+
methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
2909
|
+
customMiddleware = [],
|
|
2910
|
+
enableValidation = true
|
|
2911
|
+
} = config;
|
|
2912
|
+
const resourceName = resource.name;
|
|
2913
|
+
const basePath = `/${version}/${resourceName}`;
|
|
2914
|
+
customMiddleware.forEach((middleware) => {
|
|
2915
|
+
app.use("*", middleware);
|
|
2916
|
+
});
|
|
2917
|
+
if (resource.config?.api && typeof resource.config.api === "object") {
|
|
2918
|
+
for (const [routeDef, handler] of Object.entries(resource.config.api)) {
|
|
2919
|
+
try {
|
|
2920
|
+
const { method, path } = parseCustomRoute(routeDef);
|
|
2921
|
+
if (typeof handler !== "function") {
|
|
2922
|
+
throw new Error(`Handler for route "${routeDef}" must be a function`);
|
|
2923
|
+
}
|
|
2924
|
+
app.on(method, path, asyncHandler(async (c) => {
|
|
2925
|
+
const result = await handler(c, { resource, database: resource.database });
|
|
2926
|
+
if (result && result.constructor && result.constructor.name === "Response") {
|
|
2927
|
+
return result;
|
|
2928
|
+
}
|
|
2929
|
+
if (result !== void 0 && result !== null) {
|
|
2930
|
+
return c.json(success(result));
|
|
2931
|
+
}
|
|
2932
|
+
return c.json(noContent(), 204);
|
|
2933
|
+
}));
|
|
2934
|
+
if (config.verbose || resource.database?.verbose) {
|
|
2935
|
+
console.log(`[API Plugin] Registered custom route for ${resourceName}: ${method} ${path}`);
|
|
2936
|
+
}
|
|
2937
|
+
} catch (error) {
|
|
2938
|
+
console.error(`[API Plugin] Error registering custom route "${routeDef}" for ${resourceName}:`, error.message);
|
|
2939
|
+
throw error;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
if (methods.includes("GET")) {
|
|
2944
|
+
app.get("/", asyncHandler(async (c) => {
|
|
2945
|
+
const query = c.req.query();
|
|
2946
|
+
const limit = parseInt(query.limit) || 100;
|
|
2947
|
+
const offset = parseInt(query.offset) || 0;
|
|
2948
|
+
const partition = query.partition;
|
|
2949
|
+
const partitionValues = query.partitionValues ? JSON.parse(query.partitionValues) : void 0;
|
|
2950
|
+
const reservedKeys = ["limit", "offset", "partition", "partitionValues", "sort"];
|
|
2951
|
+
const filters = {};
|
|
2952
|
+
for (const [key, value] of Object.entries(query)) {
|
|
2953
|
+
if (!reservedKeys.includes(key)) {
|
|
2954
|
+
try {
|
|
2955
|
+
filters[key] = JSON.parse(value);
|
|
2956
|
+
} catch {
|
|
2957
|
+
filters[key] = value;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
let items;
|
|
2962
|
+
let total;
|
|
2963
|
+
if (Object.keys(filters).length > 0) {
|
|
2964
|
+
items = await resource.query(filters, { limit: limit + offset });
|
|
2965
|
+
items = items.slice(offset, offset + limit);
|
|
2966
|
+
total = items.length;
|
|
2967
|
+
} else if (partition && partitionValues) {
|
|
2968
|
+
items = await resource.listPartition({
|
|
2969
|
+
partition,
|
|
2970
|
+
partitionValues,
|
|
2971
|
+
limit: limit + offset
|
|
2972
|
+
});
|
|
2973
|
+
items = items.slice(offset, offset + limit);
|
|
2974
|
+
total = items.length;
|
|
2975
|
+
} else {
|
|
2976
|
+
items = await resource.list({ limit: limit + offset });
|
|
2977
|
+
items = items.slice(offset, offset + limit);
|
|
2978
|
+
total = items.length;
|
|
2979
|
+
}
|
|
2980
|
+
const response = list(items, {
|
|
2981
|
+
total,
|
|
2982
|
+
page: Math.floor(offset / limit) + 1,
|
|
2983
|
+
pageSize: limit,
|
|
2984
|
+
pageCount: Math.ceil(total / limit)
|
|
2985
|
+
});
|
|
2986
|
+
c.header("X-Total-Count", total.toString());
|
|
2987
|
+
c.header("X-Page-Count", Math.ceil(total / limit).toString());
|
|
2988
|
+
return c.json(response, response._status);
|
|
2989
|
+
}));
|
|
2990
|
+
}
|
|
2991
|
+
if (methods.includes("GET")) {
|
|
2992
|
+
app.get("/:id", asyncHandler(async (c) => {
|
|
2993
|
+
const id = c.req.param("id");
|
|
2994
|
+
const query = c.req.query();
|
|
2995
|
+
const partition = query.partition;
|
|
2996
|
+
const partitionValues = query.partitionValues ? JSON.parse(query.partitionValues) : void 0;
|
|
2997
|
+
let item;
|
|
2998
|
+
if (partition && partitionValues) {
|
|
2999
|
+
item = await resource.getFromPartition({
|
|
3000
|
+
id,
|
|
3001
|
+
partitionName: partition,
|
|
3002
|
+
partitionValues
|
|
3003
|
+
});
|
|
3004
|
+
} else {
|
|
3005
|
+
item = await resource.get(id);
|
|
3006
|
+
}
|
|
3007
|
+
if (!item) {
|
|
3008
|
+
const response2 = notFound(resourceName, id);
|
|
3009
|
+
return c.json(response2, response2._status);
|
|
3010
|
+
}
|
|
3011
|
+
const response = success(item);
|
|
3012
|
+
return c.json(response, response._status);
|
|
3013
|
+
}));
|
|
3014
|
+
}
|
|
3015
|
+
if (methods.includes("POST")) {
|
|
3016
|
+
app.post("/", asyncHandler(async (c) => {
|
|
3017
|
+
const data = await c.req.json();
|
|
3018
|
+
const item = await resource.insert(data);
|
|
3019
|
+
const location = `${basePath}/${item.id}`;
|
|
3020
|
+
const response = created(item, location);
|
|
3021
|
+
c.header("Location", location);
|
|
3022
|
+
return c.json(response, response._status);
|
|
3023
|
+
}));
|
|
3024
|
+
}
|
|
3025
|
+
if (methods.includes("PUT")) {
|
|
3026
|
+
app.put("/:id", asyncHandler(async (c) => {
|
|
3027
|
+
const id = c.req.param("id");
|
|
3028
|
+
const data = await c.req.json();
|
|
3029
|
+
const existing = await resource.get(id);
|
|
3030
|
+
if (!existing) {
|
|
3031
|
+
const response2 = notFound(resourceName, id);
|
|
3032
|
+
return c.json(response2, response2._status);
|
|
3033
|
+
}
|
|
3034
|
+
const updated = await resource.update(id, data);
|
|
3035
|
+
const response = success(updated);
|
|
3036
|
+
return c.json(response, response._status);
|
|
3037
|
+
}));
|
|
3038
|
+
}
|
|
3039
|
+
if (methods.includes("PATCH")) {
|
|
3040
|
+
app.patch("/:id", asyncHandler(async (c) => {
|
|
3041
|
+
const id = c.req.param("id");
|
|
3042
|
+
const data = await c.req.json();
|
|
3043
|
+
const existing = await resource.get(id);
|
|
3044
|
+
if (!existing) {
|
|
3045
|
+
const response2 = notFound(resourceName, id);
|
|
3046
|
+
return c.json(response2, response2._status);
|
|
3047
|
+
}
|
|
3048
|
+
const merged = { ...existing, ...data, id };
|
|
3049
|
+
const updated = await resource.update(id, merged);
|
|
3050
|
+
const response = success(updated);
|
|
3051
|
+
return c.json(response, response._status);
|
|
3052
|
+
}));
|
|
3053
|
+
}
|
|
3054
|
+
if (methods.includes("DELETE")) {
|
|
3055
|
+
app.delete("/:id", asyncHandler(async (c) => {
|
|
3056
|
+
const id = c.req.param("id");
|
|
3057
|
+
const existing = await resource.get(id);
|
|
3058
|
+
if (!existing) {
|
|
3059
|
+
const response2 = notFound(resourceName, id);
|
|
3060
|
+
return c.json(response2, response2._status);
|
|
3061
|
+
}
|
|
3062
|
+
await resource.delete(id);
|
|
3063
|
+
const response = noContent();
|
|
3064
|
+
return c.json(response, response._status);
|
|
3065
|
+
}));
|
|
3066
|
+
}
|
|
3067
|
+
if (methods.includes("HEAD")) {
|
|
3068
|
+
app.on("HEAD", "/", asyncHandler(async (c) => {
|
|
3069
|
+
const total = await resource.count();
|
|
3070
|
+
const allItems = await resource.list({ limit: 1e3 });
|
|
3071
|
+
const stats = {
|
|
3072
|
+
total,
|
|
3073
|
+
version: resource.config?.currentVersion || resource.version || "v1"
|
|
3074
|
+
};
|
|
3075
|
+
c.header("X-Total-Count", total.toString());
|
|
3076
|
+
c.header("X-Resource-Version", stats.version);
|
|
3077
|
+
c.header("X-Schema-Fields", Object.keys(resource.config?.attributes || {}).length.toString());
|
|
3078
|
+
return c.body(null, 200);
|
|
3079
|
+
}));
|
|
3080
|
+
app.on("HEAD", "/:id", asyncHandler(async (c) => {
|
|
3081
|
+
const id = c.req.param("id");
|
|
3082
|
+
const item = await resource.get(id);
|
|
3083
|
+
if (!item) {
|
|
3084
|
+
return c.body(null, 404);
|
|
3085
|
+
}
|
|
3086
|
+
if (item.updatedAt) {
|
|
3087
|
+
c.header("Last-Modified", new Date(item.updatedAt).toUTCString());
|
|
3088
|
+
}
|
|
3089
|
+
return c.body(null, 200);
|
|
3090
|
+
}));
|
|
3091
|
+
}
|
|
3092
|
+
if (methods.includes("OPTIONS")) {
|
|
3093
|
+
app.options("/", asyncHandler(async (c) => {
|
|
3094
|
+
c.header("Allow", methods.join(", "));
|
|
3095
|
+
const total = await resource.count();
|
|
3096
|
+
const schema = resource.config?.attributes || {};
|
|
3097
|
+
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
3098
|
+
const metadata = {
|
|
3099
|
+
resource: resourceName,
|
|
3100
|
+
version: version2,
|
|
3101
|
+
totalRecords: total,
|
|
3102
|
+
allowedMethods: methods,
|
|
3103
|
+
schema: Object.entries(schema).map(([name, def]) => ({
|
|
3104
|
+
name,
|
|
3105
|
+
type: typeof def === "string" ? def.split("|")[0] : def.type,
|
|
3106
|
+
rules: typeof def === "string" ? def.split("|").slice(1) : []
|
|
3107
|
+
})),
|
|
3108
|
+
endpoints: {
|
|
3109
|
+
list: `/${version2}/${resourceName}`,
|
|
3110
|
+
get: `/${version2}/${resourceName}/:id`,
|
|
3111
|
+
create: `/${version2}/${resourceName}`,
|
|
3112
|
+
update: `/${version2}/${resourceName}/:id`,
|
|
3113
|
+
delete: `/${version2}/${resourceName}/:id`
|
|
3114
|
+
},
|
|
3115
|
+
queryParameters: {
|
|
3116
|
+
limit: "number (1-1000, default: 100)",
|
|
3117
|
+
offset: "number (min: 0, default: 0)",
|
|
3118
|
+
partition: "string (partition name)",
|
|
3119
|
+
partitionValues: "JSON string",
|
|
3120
|
+
"[any field]": "any (filter by field value)"
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
return c.json(metadata);
|
|
3124
|
+
}));
|
|
3125
|
+
app.options("/:id", (c) => {
|
|
3126
|
+
c.header("Allow", methods.filter((m) => m !== "POST").join(", "));
|
|
3127
|
+
return c.body(null, 204);
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
return app;
|
|
3131
|
+
}
|
|
3132
|
+
function createRelationalRoutes(sourceResource, relationName, relationConfig, version, Hono) {
|
|
3133
|
+
const app = new Hono();
|
|
3134
|
+
const resourceName = sourceResource.name;
|
|
3135
|
+
const relatedResourceName = relationConfig.resource;
|
|
3136
|
+
app.get("/:id", asyncHandler(async (c) => {
|
|
3137
|
+
const id = c.req.param("id");
|
|
3138
|
+
const query = c.req.query();
|
|
3139
|
+
const source = await sourceResource.get(id);
|
|
3140
|
+
if (!source) {
|
|
3141
|
+
const response = notFound(resourceName, id);
|
|
3142
|
+
return c.json(response, response._status);
|
|
3143
|
+
}
|
|
3144
|
+
const result = await sourceResource.get(id, {
|
|
3145
|
+
include: [relationName]
|
|
3146
|
+
});
|
|
3147
|
+
const relatedData = result[relationName];
|
|
3148
|
+
if (!relatedData) {
|
|
3149
|
+
if (relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany") {
|
|
3150
|
+
const response = list([], {
|
|
3151
|
+
total: 0,
|
|
3152
|
+
page: 1,
|
|
3153
|
+
pageSize: 100,
|
|
3154
|
+
pageCount: 0
|
|
3155
|
+
});
|
|
3156
|
+
return c.json(response, response._status);
|
|
3157
|
+
} else {
|
|
3158
|
+
const response = notFound(relatedResourceName, "related resource");
|
|
3159
|
+
return c.json(response, response._status);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
if (relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany") {
|
|
3163
|
+
const items = Array.isArray(relatedData) ? relatedData : [relatedData];
|
|
3164
|
+
const limit = parseInt(query.limit) || 100;
|
|
3165
|
+
const offset = parseInt(query.offset) || 0;
|
|
3166
|
+
const paginatedItems = items.slice(offset, offset + limit);
|
|
3167
|
+
const response = list(paginatedItems, {
|
|
3168
|
+
total: items.length,
|
|
3169
|
+
page: Math.floor(offset / limit) + 1,
|
|
3170
|
+
pageSize: limit,
|
|
3171
|
+
pageCount: Math.ceil(items.length / limit)
|
|
3172
|
+
});
|
|
3173
|
+
c.header("X-Total-Count", items.length.toString());
|
|
3174
|
+
c.header("X-Page-Count", Math.ceil(items.length / limit).toString());
|
|
3175
|
+
return c.json(response, response._status);
|
|
3176
|
+
} else {
|
|
3177
|
+
const response = success(relatedData);
|
|
3178
|
+
return c.json(response, response._status);
|
|
3179
|
+
}
|
|
3180
|
+
}));
|
|
3181
|
+
return app;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
function mapFieldTypeToOpenAPI(fieldType) {
|
|
3185
|
+
const type = fieldType.split("|")[0].trim();
|
|
3186
|
+
const typeMap = {
|
|
3187
|
+
"string": { type: "string" },
|
|
3188
|
+
"number": { type: "number" },
|
|
3189
|
+
"integer": { type: "integer" },
|
|
3190
|
+
"boolean": { type: "boolean" },
|
|
3191
|
+
"array": { type: "array", items: { type: "string" } },
|
|
3192
|
+
"object": { type: "object" },
|
|
3193
|
+
"json": { type: "object" },
|
|
3194
|
+
"secret": { type: "string", format: "password" },
|
|
3195
|
+
"email": { type: "string", format: "email" },
|
|
3196
|
+
"url": { type: "string", format: "uri" },
|
|
3197
|
+
"date": { type: "string", format: "date" },
|
|
3198
|
+
"datetime": { type: "string", format: "date-time" },
|
|
3199
|
+
"ip4": { type: "string", format: "ipv4", description: "IPv4 address" },
|
|
3200
|
+
"ip6": { type: "string", format: "ipv6", description: "IPv6 address" },
|
|
3201
|
+
"embedding": { type: "array", items: { type: "number" }, description: "Vector embedding" }
|
|
3202
|
+
};
|
|
3203
|
+
if (type.startsWith("embedding:")) {
|
|
3204
|
+
const length = parseInt(type.split(":")[1]);
|
|
3205
|
+
return {
|
|
3206
|
+
type: "array",
|
|
3207
|
+
items: { type: "number" },
|
|
3208
|
+
minItems: length,
|
|
3209
|
+
maxItems: length,
|
|
3210
|
+
description: `Vector embedding (${length} dimensions)`
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
return typeMap[type] || { type: "string" };
|
|
3214
|
+
}
|
|
3215
|
+
function extractValidationRules(fieldDef) {
|
|
3216
|
+
const rules = {};
|
|
3217
|
+
const parts = fieldDef.split("|");
|
|
3218
|
+
for (const part of parts) {
|
|
3219
|
+
const [rule, value] = part.split(":").map((s) => s.trim());
|
|
3220
|
+
switch (rule) {
|
|
3221
|
+
case "required":
|
|
3222
|
+
rules.required = true;
|
|
3223
|
+
break;
|
|
3224
|
+
case "min":
|
|
3225
|
+
rules.minimum = parseFloat(value);
|
|
3226
|
+
break;
|
|
3227
|
+
case "max":
|
|
3228
|
+
rules.maximum = parseFloat(value);
|
|
3229
|
+
break;
|
|
3230
|
+
case "minlength":
|
|
3231
|
+
rules.minLength = parseInt(value);
|
|
3232
|
+
break;
|
|
3233
|
+
case "maxlength":
|
|
3234
|
+
rules.maxLength = parseInt(value);
|
|
3235
|
+
break;
|
|
3236
|
+
case "pattern":
|
|
3237
|
+
rules.pattern = value;
|
|
3238
|
+
break;
|
|
3239
|
+
case "enum":
|
|
3240
|
+
rules.enum = value.split(",").map((v) => v.trim());
|
|
3241
|
+
break;
|
|
3242
|
+
case "default":
|
|
3243
|
+
rules.default = value;
|
|
3244
|
+
break;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return rules;
|
|
3248
|
+
}
|
|
3249
|
+
function generateResourceSchema(resource) {
|
|
3250
|
+
const properties = {};
|
|
3251
|
+
const required = [];
|
|
3252
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
3253
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
3254
|
+
const attributes = Object.fromEntries(
|
|
3255
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
3256
|
+
);
|
|
3257
|
+
const resourceDescription = resource.config?.description;
|
|
3258
|
+
const attributeDescriptions = typeof resourceDescription === "object" ? resourceDescription.attributes || {} : {};
|
|
3259
|
+
properties.id = {
|
|
3260
|
+
type: "string",
|
|
3261
|
+
description: "Unique identifier for the resource",
|
|
3262
|
+
example: "2_gDTpeU6EI0e8B92n_R3Y",
|
|
3263
|
+
readOnly: true
|
|
3264
|
+
};
|
|
3265
|
+
for (const [fieldName, fieldDef] of Object.entries(attributes)) {
|
|
3266
|
+
if (typeof fieldDef === "object" && fieldDef.type) {
|
|
3267
|
+
const baseType = mapFieldTypeToOpenAPI(fieldDef.type);
|
|
3268
|
+
properties[fieldName] = {
|
|
3269
|
+
...baseType,
|
|
3270
|
+
description: fieldDef.description || attributeDescriptions[fieldName] || void 0
|
|
3271
|
+
};
|
|
3272
|
+
if (fieldDef.required) {
|
|
3273
|
+
required.push(fieldName);
|
|
3274
|
+
}
|
|
3275
|
+
if (fieldDef.type === "object" && fieldDef.props) {
|
|
3276
|
+
properties[fieldName].properties = {};
|
|
3277
|
+
for (const [propName, propDef] of Object.entries(fieldDef.props)) {
|
|
3278
|
+
const propType = typeof propDef === "string" ? propDef : propDef.type;
|
|
3279
|
+
properties[fieldName].properties[propName] = mapFieldTypeToOpenAPI(propType);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
if (fieldDef.type === "array" && fieldDef.items) {
|
|
3283
|
+
properties[fieldName].items = mapFieldTypeToOpenAPI(fieldDef.items);
|
|
3284
|
+
}
|
|
3285
|
+
} else if (typeof fieldDef === "string") {
|
|
3286
|
+
const baseType = mapFieldTypeToOpenAPI(fieldDef);
|
|
3287
|
+
const rules = extractValidationRules(fieldDef);
|
|
3288
|
+
properties[fieldName] = {
|
|
3289
|
+
...baseType,
|
|
3290
|
+
...rules,
|
|
3291
|
+
description: attributeDescriptions[fieldName] || void 0
|
|
3292
|
+
};
|
|
3293
|
+
if (rules.required) {
|
|
3294
|
+
required.push(fieldName);
|
|
3295
|
+
delete properties[fieldName].required;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
return {
|
|
3300
|
+
type: "object",
|
|
3301
|
+
properties,
|
|
3302
|
+
required: required.length > 0 ? required : void 0
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
function generateResourcePaths(resource, version, config = {}) {
|
|
3306
|
+
const resourceName = resource.name;
|
|
3307
|
+
const basePath = `/${version}/${resourceName}`;
|
|
3308
|
+
const schema = generateResourceSchema(resource);
|
|
3309
|
+
const methods = config.methods || ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
3310
|
+
const authMethods = config.auth || [];
|
|
3311
|
+
const requiresAuth = authMethods && authMethods.length > 0;
|
|
3312
|
+
const paths = {};
|
|
3313
|
+
const security = [];
|
|
3314
|
+
if (requiresAuth) {
|
|
3315
|
+
if (authMethods.includes("jwt")) security.push({ bearerAuth: [] });
|
|
3316
|
+
if (authMethods.includes("apiKey")) security.push({ apiKeyAuth: [] });
|
|
3317
|
+
if (authMethods.includes("basic")) security.push({ basicAuth: [] });
|
|
3318
|
+
}
|
|
3319
|
+
const partitions = resource.config?.options?.partitions || resource.config?.partitions || resource.partitions || {};
|
|
3320
|
+
const partitionNames = Object.keys(partitions);
|
|
3321
|
+
const hasPartitions = partitionNames.length > 0;
|
|
3322
|
+
let partitionDescription = "Partition name for filtering";
|
|
3323
|
+
let partitionValuesDescription = "Partition values as JSON string";
|
|
3324
|
+
let partitionExample = void 0;
|
|
3325
|
+
let partitionValuesExample = void 0;
|
|
3326
|
+
if (hasPartitions) {
|
|
3327
|
+
const partitionDocs = partitionNames.map((name) => {
|
|
3328
|
+
const partition = partitions[name];
|
|
3329
|
+
const fields = Object.keys(partition.fields || {});
|
|
3330
|
+
const fieldTypes = Object.entries(partition.fields || {}).map(([field, type]) => `${field}: ${type}`).join(", ");
|
|
3331
|
+
return `- **${name}**: Filters by ${fields.join(", ")} (${fieldTypes})`;
|
|
3332
|
+
}).join("\n");
|
|
3333
|
+
partitionDescription = `Available partitions:
|
|
3334
|
+
${partitionDocs}`;
|
|
3335
|
+
const examplePartition = partitionNames[0];
|
|
3336
|
+
const exampleFields = partitions[examplePartition]?.fields || {};
|
|
3337
|
+
Object.entries(exampleFields).map(([field, type]) => `"${field}": <${type} value>`).join(", ");
|
|
3338
|
+
partitionValuesDescription = `Partition field values as JSON string. Must match the structure of the selected partition.
|
|
3339
|
+
|
|
3340
|
+
Example for "${examplePartition}" partition: \`{"${Object.keys(exampleFields)[0]}": "value"}\``;
|
|
3341
|
+
partitionExample = examplePartition;
|
|
3342
|
+
const firstField = Object.keys(exampleFields)[0];
|
|
3343
|
+
const firstFieldType = exampleFields[firstField];
|
|
3344
|
+
let exampleValue = "example";
|
|
3345
|
+
if (firstFieldType === "number" || firstFieldType === "integer") {
|
|
3346
|
+
exampleValue = 123;
|
|
3347
|
+
} else if (firstFieldType === "boolean") {
|
|
3348
|
+
exampleValue = true;
|
|
3349
|
+
}
|
|
3350
|
+
partitionValuesExample = JSON.stringify({ [firstField]: exampleValue });
|
|
3351
|
+
}
|
|
3352
|
+
const attributeQueryParams = [];
|
|
3353
|
+
if (hasPartitions) {
|
|
3354
|
+
const partitionFieldsSet = /* @__PURE__ */ new Set();
|
|
3355
|
+
for (const [partitionName, partition] of Object.entries(partitions)) {
|
|
3356
|
+
const fields = partition.fields || {};
|
|
3357
|
+
for (const fieldName of Object.keys(fields)) {
|
|
3358
|
+
partitionFieldsSet.add(fieldName);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
const allAttributes = resource.config?.attributes || resource.attributes || {};
|
|
3362
|
+
const pluginAttrNames = resource.schema?._pluginAttributes ? Object.values(resource.schema._pluginAttributes).flat() : [];
|
|
3363
|
+
const attributes = Object.fromEntries(
|
|
3364
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
3365
|
+
);
|
|
3366
|
+
for (const fieldName of partitionFieldsSet) {
|
|
3367
|
+
const fieldDef = attributes[fieldName];
|
|
3368
|
+
if (!fieldDef) continue;
|
|
3369
|
+
let fieldType;
|
|
3370
|
+
if (typeof fieldDef === "object" && fieldDef.type) {
|
|
3371
|
+
fieldType = fieldDef.type;
|
|
3372
|
+
} else if (typeof fieldDef === "string") {
|
|
3373
|
+
fieldType = fieldDef.split("|")[0].trim();
|
|
3374
|
+
} else {
|
|
3375
|
+
fieldType = "string";
|
|
3376
|
+
}
|
|
3377
|
+
const openAPIType = mapFieldTypeToOpenAPI(fieldType);
|
|
3378
|
+
attributeQueryParams.push({
|
|
3379
|
+
name: fieldName,
|
|
3380
|
+
in: "query",
|
|
3381
|
+
description: `Filter by ${fieldName} field (indexed via partitions for efficient querying). Value will be parsed as JSON if possible, otherwise treated as string.`,
|
|
3382
|
+
required: false,
|
|
3383
|
+
schema: openAPIType
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
if (methods.includes("GET")) {
|
|
3388
|
+
paths[basePath] = {
|
|
3389
|
+
get: {
|
|
3390
|
+
tags: [resourceName],
|
|
3391
|
+
summary: `List ${resourceName}`,
|
|
3392
|
+
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.
|
|
3393
|
+
|
|
3394
|
+
**Pagination**: Use \`limit\` and \`offset\` to paginate results. For example:
|
|
3395
|
+
- First page (10 items): \`?limit=10&offset=0\`
|
|
3396
|
+
- Second page: \`?limit=10&offset=10\`
|
|
3397
|
+
- Third page: \`?limit=10&offset=20\`
|
|
3398
|
+
|
|
3399
|
+
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." : ""}`,
|
|
3400
|
+
parameters: [
|
|
3401
|
+
{
|
|
3402
|
+
name: "limit",
|
|
3403
|
+
in: "query",
|
|
3404
|
+
description: "Maximum number of items to return per page (page size)",
|
|
3405
|
+
schema: { type: "integer", default: 100, minimum: 1, maximum: 1e3 },
|
|
3406
|
+
example: 10
|
|
3407
|
+
},
|
|
3408
|
+
{
|
|
3409
|
+
name: "offset",
|
|
3410
|
+
in: "query",
|
|
3411
|
+
description: "Number of items to skip before starting to return results. Use for pagination: offset = (page - 1) * limit",
|
|
3412
|
+
schema: { type: "integer", default: 0, minimum: 0 },
|
|
3413
|
+
example: 0
|
|
3414
|
+
},
|
|
3415
|
+
...hasPartitions ? [
|
|
3416
|
+
{
|
|
3417
|
+
name: "partition",
|
|
3418
|
+
in: "query",
|
|
3419
|
+
description: partitionDescription,
|
|
3420
|
+
schema: {
|
|
3421
|
+
type: "string",
|
|
3422
|
+
enum: partitionNames
|
|
3423
|
+
},
|
|
3424
|
+
example: partitionExample
|
|
3425
|
+
},
|
|
3426
|
+
{
|
|
3427
|
+
name: "partitionValues",
|
|
3428
|
+
in: "query",
|
|
3429
|
+
description: partitionValuesDescription,
|
|
3430
|
+
schema: { type: "string" },
|
|
3431
|
+
example: partitionValuesExample
|
|
3432
|
+
}
|
|
3433
|
+
] : [],
|
|
3434
|
+
...attributeQueryParams
|
|
3435
|
+
],
|
|
3436
|
+
responses: {
|
|
3437
|
+
200: {
|
|
3438
|
+
description: "Successful response",
|
|
3439
|
+
content: {
|
|
3440
|
+
"application/json": {
|
|
3441
|
+
schema: {
|
|
3442
|
+
type: "object",
|
|
3443
|
+
properties: {
|
|
3444
|
+
success: { type: "boolean", example: true },
|
|
3445
|
+
data: {
|
|
3446
|
+
type: "array",
|
|
3447
|
+
items: schema
|
|
3448
|
+
},
|
|
3449
|
+
pagination: {
|
|
3450
|
+
type: "object",
|
|
3451
|
+
description: "Pagination metadata for the current request",
|
|
3452
|
+
properties: {
|
|
3453
|
+
total: {
|
|
3454
|
+
type: "integer",
|
|
3455
|
+
description: "Total number of items available",
|
|
3456
|
+
example: 150
|
|
3457
|
+
},
|
|
3458
|
+
page: {
|
|
3459
|
+
type: "integer",
|
|
3460
|
+
description: "Current page number (1-indexed)",
|
|
3461
|
+
example: 1
|
|
3462
|
+
},
|
|
3463
|
+
pageSize: {
|
|
3464
|
+
type: "integer",
|
|
3465
|
+
description: "Number of items per page (same as limit parameter)",
|
|
3466
|
+
example: 10
|
|
3467
|
+
},
|
|
3468
|
+
pageCount: {
|
|
3469
|
+
type: "integer",
|
|
3470
|
+
description: "Total number of pages available",
|
|
3471
|
+
example: 15
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
},
|
|
3479
|
+
headers: {
|
|
3480
|
+
"X-Total-Count": {
|
|
3481
|
+
description: "Total number of records",
|
|
3482
|
+
schema: { type: "integer" }
|
|
3483
|
+
},
|
|
3484
|
+
"X-Page-Count": {
|
|
3485
|
+
description: "Total number of pages",
|
|
3486
|
+
schema: { type: "integer" }
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
},
|
|
3491
|
+
security: security.length > 0 ? security : void 0
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
if (methods.includes("GET")) {
|
|
3496
|
+
paths[`${basePath}/{id}`] = {
|
|
3497
|
+
get: {
|
|
3498
|
+
tags: [resourceName],
|
|
3499
|
+
summary: `Get ${resourceName} by ID`,
|
|
3500
|
+
description: `Retrieve a single ${resourceName} by its ID${hasPartitions ? ". Optionally specify a partition for more efficient retrieval." : ""}`,
|
|
3501
|
+
parameters: [
|
|
3502
|
+
{
|
|
3503
|
+
name: "id",
|
|
3504
|
+
in: "path",
|
|
3505
|
+
required: true,
|
|
3506
|
+
description: `${resourceName} ID`,
|
|
3507
|
+
schema: { type: "string" }
|
|
3508
|
+
},
|
|
3509
|
+
...hasPartitions ? [
|
|
3510
|
+
{
|
|
3511
|
+
name: "partition",
|
|
3512
|
+
in: "query",
|
|
3513
|
+
description: partitionDescription,
|
|
3514
|
+
schema: {
|
|
3515
|
+
type: "string",
|
|
3516
|
+
enum: partitionNames
|
|
3517
|
+
},
|
|
3518
|
+
example: partitionExample
|
|
3519
|
+
},
|
|
3520
|
+
{
|
|
3521
|
+
name: "partitionValues",
|
|
3522
|
+
in: "query",
|
|
3523
|
+
description: partitionValuesDescription,
|
|
3524
|
+
schema: { type: "string" },
|
|
3525
|
+
example: partitionValuesExample
|
|
3526
|
+
}
|
|
3527
|
+
] : []
|
|
3528
|
+
],
|
|
3529
|
+
responses: {
|
|
3530
|
+
200: {
|
|
3531
|
+
description: "Successful response",
|
|
3532
|
+
content: {
|
|
3533
|
+
"application/json": {
|
|
3534
|
+
schema: {
|
|
3535
|
+
type: "object",
|
|
3536
|
+
properties: {
|
|
3537
|
+
success: { type: "boolean", example: true },
|
|
3538
|
+
data: schema
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
},
|
|
3544
|
+
404: {
|
|
3545
|
+
description: "Resource not found",
|
|
3546
|
+
content: {
|
|
3547
|
+
"application/json": {
|
|
3548
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
},
|
|
3553
|
+
security: security.length > 0 ? security : void 0
|
|
3554
|
+
}
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
if (methods.includes("POST")) {
|
|
3558
|
+
if (!paths[basePath]) paths[basePath] = {};
|
|
3559
|
+
paths[basePath].post = {
|
|
3560
|
+
tags: [resourceName],
|
|
3561
|
+
summary: `Create ${resourceName}`,
|
|
3562
|
+
description: `Create a new ${resourceName}`,
|
|
3563
|
+
requestBody: {
|
|
3564
|
+
required: true,
|
|
3565
|
+
content: {
|
|
3566
|
+
"application/json": {
|
|
3567
|
+
schema
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
},
|
|
3571
|
+
responses: {
|
|
3572
|
+
201: {
|
|
3573
|
+
description: "Resource created successfully",
|
|
3574
|
+
content: {
|
|
3575
|
+
"application/json": {
|
|
3576
|
+
schema: {
|
|
3577
|
+
type: "object",
|
|
3578
|
+
properties: {
|
|
3579
|
+
success: { type: "boolean", example: true },
|
|
3580
|
+
data: schema
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
},
|
|
3585
|
+
headers: {
|
|
3586
|
+
Location: {
|
|
3587
|
+
description: "URL of the created resource",
|
|
3588
|
+
schema: { type: "string" }
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
},
|
|
3592
|
+
400: {
|
|
3593
|
+
description: "Validation error",
|
|
3594
|
+
content: {
|
|
3595
|
+
"application/json": {
|
|
3596
|
+
schema: { $ref: "#/components/schemas/ValidationError" }
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
},
|
|
3601
|
+
security: security.length > 0 ? security : void 0
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
if (methods.includes("PUT")) {
|
|
3605
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3606
|
+
paths[`${basePath}/{id}`].put = {
|
|
3607
|
+
tags: [resourceName],
|
|
3608
|
+
summary: `Update ${resourceName} (full)`,
|
|
3609
|
+
description: `Fully update a ${resourceName}`,
|
|
3610
|
+
parameters: [
|
|
3611
|
+
{
|
|
3612
|
+
name: "id",
|
|
3613
|
+
in: "path",
|
|
3614
|
+
required: true,
|
|
3615
|
+
schema: { type: "string" }
|
|
3616
|
+
}
|
|
3617
|
+
],
|
|
3618
|
+
requestBody: {
|
|
3619
|
+
required: true,
|
|
3620
|
+
content: {
|
|
3621
|
+
"application/json": {
|
|
3622
|
+
schema
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
},
|
|
3626
|
+
responses: {
|
|
3627
|
+
200: {
|
|
3628
|
+
description: "Resource updated successfully",
|
|
3629
|
+
content: {
|
|
3630
|
+
"application/json": {
|
|
3631
|
+
schema: {
|
|
3632
|
+
type: "object",
|
|
3633
|
+
properties: {
|
|
3634
|
+
success: { type: "boolean", example: true },
|
|
3635
|
+
data: schema
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
},
|
|
3641
|
+
404: {
|
|
3642
|
+
description: "Resource not found",
|
|
3643
|
+
content: {
|
|
3644
|
+
"application/json": {
|
|
3645
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
},
|
|
3650
|
+
security: security.length > 0 ? security : void 0
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
if (methods.includes("PATCH")) {
|
|
3654
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3655
|
+
paths[`${basePath}/{id}`].patch = {
|
|
3656
|
+
tags: [resourceName],
|
|
3657
|
+
summary: `Update ${resourceName} (partial)`,
|
|
3658
|
+
description: `Partially update a ${resourceName}`,
|
|
3659
|
+
parameters: [
|
|
3660
|
+
{
|
|
3661
|
+
name: "id",
|
|
3662
|
+
in: "path",
|
|
3663
|
+
required: true,
|
|
3664
|
+
schema: { type: "string" }
|
|
3665
|
+
}
|
|
3666
|
+
],
|
|
3667
|
+
requestBody: {
|
|
3668
|
+
required: true,
|
|
3669
|
+
content: {
|
|
3670
|
+
"application/json": {
|
|
3671
|
+
schema: {
|
|
3672
|
+
...schema,
|
|
3673
|
+
required: void 0
|
|
3674
|
+
// Partial updates don't require all fields
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
},
|
|
3679
|
+
responses: {
|
|
3680
|
+
200: {
|
|
3681
|
+
description: "Resource updated successfully",
|
|
3682
|
+
content: {
|
|
3683
|
+
"application/json": {
|
|
3684
|
+
schema: {
|
|
3685
|
+
type: "object",
|
|
3686
|
+
properties: {
|
|
3687
|
+
success: { type: "boolean", example: true },
|
|
3688
|
+
data: schema
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
},
|
|
3694
|
+
404: {
|
|
3695
|
+
description: "Resource not found",
|
|
3696
|
+
content: {
|
|
3697
|
+
"application/json": {
|
|
3698
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
},
|
|
3703
|
+
security: security.length > 0 ? security : void 0
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
if (methods.includes("DELETE")) {
|
|
3707
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3708
|
+
paths[`${basePath}/{id}`].delete = {
|
|
3709
|
+
tags: [resourceName],
|
|
3710
|
+
summary: `Delete ${resourceName}`,
|
|
3711
|
+
description: `Delete a ${resourceName} by ID`,
|
|
3712
|
+
parameters: [
|
|
3713
|
+
{
|
|
3714
|
+
name: "id",
|
|
3715
|
+
in: "path",
|
|
3716
|
+
required: true,
|
|
3717
|
+
schema: { type: "string" }
|
|
3718
|
+
}
|
|
3719
|
+
],
|
|
3720
|
+
responses: {
|
|
3721
|
+
204: {
|
|
3722
|
+
description: "Resource deleted successfully"
|
|
3723
|
+
},
|
|
3724
|
+
404: {
|
|
3725
|
+
description: "Resource not found",
|
|
3726
|
+
content: {
|
|
3727
|
+
"application/json": {
|
|
3728
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
},
|
|
3733
|
+
security: security.length > 0 ? security : void 0
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
3736
|
+
if (methods.includes("HEAD")) {
|
|
3737
|
+
if (!paths[basePath]) paths[basePath] = {};
|
|
3738
|
+
paths[basePath].head = {
|
|
3739
|
+
tags: [resourceName],
|
|
3740
|
+
summary: `Get ${resourceName} statistics`,
|
|
3741
|
+
description: `Get statistics about ${resourceName} collection without retrieving data. Returns statistics in response headers.`,
|
|
3742
|
+
responses: {
|
|
3743
|
+
200: {
|
|
3744
|
+
description: "Statistics retrieved successfully",
|
|
3745
|
+
headers: {
|
|
3746
|
+
"X-Total-Count": {
|
|
3747
|
+
description: "Total number of records",
|
|
3748
|
+
schema: { type: "integer" }
|
|
3749
|
+
},
|
|
3750
|
+
"X-Resource-Version": {
|
|
3751
|
+
description: "Current resource version",
|
|
3752
|
+
schema: { type: "string" }
|
|
3753
|
+
},
|
|
3754
|
+
"X-Schema-Fields": {
|
|
3755
|
+
description: "Number of schema fields",
|
|
3756
|
+
schema: { type: "integer" }
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
},
|
|
3761
|
+
security: security.length > 0 ? security : void 0
|
|
3762
|
+
};
|
|
3763
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3764
|
+
paths[`${basePath}/{id}`].head = {
|
|
3765
|
+
tags: [resourceName],
|
|
3766
|
+
summary: `Check if ${resourceName} exists`,
|
|
3767
|
+
description: `Check if a ${resourceName} exists without retrieving its data`,
|
|
3768
|
+
parameters: [
|
|
3769
|
+
{
|
|
3770
|
+
name: "id",
|
|
3771
|
+
in: "path",
|
|
3772
|
+
required: true,
|
|
3773
|
+
schema: { type: "string" }
|
|
3774
|
+
}
|
|
3775
|
+
],
|
|
3776
|
+
responses: {
|
|
3777
|
+
200: {
|
|
3778
|
+
description: "Resource exists",
|
|
3779
|
+
headers: {
|
|
3780
|
+
"Last-Modified": {
|
|
3781
|
+
description: "Last modification date",
|
|
3782
|
+
schema: { type: "string", format: "date-time" }
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
},
|
|
3786
|
+
404: {
|
|
3787
|
+
description: "Resource not found"
|
|
3788
|
+
}
|
|
3789
|
+
},
|
|
3790
|
+
security: security.length > 0 ? security : void 0
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
if (methods.includes("OPTIONS")) {
|
|
3794
|
+
if (!paths[basePath]) paths[basePath] = {};
|
|
3795
|
+
paths[basePath].options = {
|
|
3796
|
+
tags: [resourceName],
|
|
3797
|
+
summary: `Get ${resourceName} metadata`,
|
|
3798
|
+
description: `Get complete metadata about ${resourceName} resource including schema, allowed methods, endpoints, and query parameters`,
|
|
3799
|
+
responses: {
|
|
3800
|
+
200: {
|
|
3801
|
+
description: "Metadata retrieved successfully",
|
|
3802
|
+
headers: {
|
|
3803
|
+
"Allow": {
|
|
3804
|
+
description: "Allowed HTTP methods",
|
|
3805
|
+
schema: { type: "string", example: "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS" }
|
|
3806
|
+
}
|
|
3807
|
+
},
|
|
3808
|
+
content: {
|
|
3809
|
+
"application/json": {
|
|
3810
|
+
schema: {
|
|
3811
|
+
type: "object",
|
|
3812
|
+
properties: {
|
|
3813
|
+
resource: { type: "string" },
|
|
3814
|
+
version: { type: "string" },
|
|
3815
|
+
totalRecords: { type: "integer" },
|
|
3816
|
+
allowedMethods: {
|
|
3817
|
+
type: "array",
|
|
3818
|
+
items: { type: "string" }
|
|
3819
|
+
},
|
|
3820
|
+
schema: {
|
|
3821
|
+
type: "array",
|
|
3822
|
+
items: {
|
|
3823
|
+
type: "object",
|
|
3824
|
+
properties: {
|
|
3825
|
+
name: { type: "string" },
|
|
3826
|
+
type: { type: "string" },
|
|
3827
|
+
rules: { type: "array", items: { type: "string" } }
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
},
|
|
3831
|
+
endpoints: {
|
|
3832
|
+
type: "object",
|
|
3833
|
+
properties: {
|
|
3834
|
+
list: { type: "string" },
|
|
3835
|
+
get: { type: "string" },
|
|
3836
|
+
create: { type: "string" },
|
|
3837
|
+
update: { type: "string" },
|
|
3838
|
+
delete: { type: "string" }
|
|
3839
|
+
}
|
|
3840
|
+
},
|
|
3841
|
+
queryParameters: { type: "object" }
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
if (!paths[`${basePath}/{id}`]) paths[`${basePath}/{id}`] = {};
|
|
3850
|
+
paths[`${basePath}/{id}`].options = {
|
|
3851
|
+
tags: [resourceName],
|
|
3852
|
+
summary: `Get allowed methods for ${resourceName} item`,
|
|
3853
|
+
description: `Get allowed HTTP methods for individual ${resourceName} operations`,
|
|
3854
|
+
parameters: [
|
|
3855
|
+
{
|
|
3856
|
+
name: "id",
|
|
3857
|
+
in: "path",
|
|
3858
|
+
required: true,
|
|
3859
|
+
schema: { type: "string" }
|
|
3860
|
+
}
|
|
3861
|
+
],
|
|
3862
|
+
responses: {
|
|
3863
|
+
204: {
|
|
3864
|
+
description: "Methods retrieved successfully",
|
|
3865
|
+
headers: {
|
|
3866
|
+
"Allow": {
|
|
3867
|
+
description: "Allowed HTTP methods",
|
|
3868
|
+
schema: { type: "string", example: "GET, PUT, PATCH, DELETE, HEAD, OPTIONS" }
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
return paths;
|
|
3876
|
+
}
|
|
3877
|
+
function generateRelationalPaths(resource, relationName, relationConfig, version, relatedSchema) {
|
|
3878
|
+
const resourceName = resource.name;
|
|
3879
|
+
const basePath = `/${version}/${resourceName}/{id}/${relationName}`;
|
|
3880
|
+
relationConfig.resource;
|
|
3881
|
+
const isToMany = relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany";
|
|
3882
|
+
const paths = {};
|
|
3883
|
+
paths[basePath] = {
|
|
3884
|
+
get: {
|
|
3885
|
+
tags: [resourceName],
|
|
3886
|
+
summary: `Get ${relationName} of ${resourceName}`,
|
|
3887
|
+
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.` : "."),
|
|
3888
|
+
parameters: [
|
|
3889
|
+
{
|
|
3890
|
+
name: "id",
|
|
3891
|
+
in: "path",
|
|
3892
|
+
required: true,
|
|
3893
|
+
description: `${resourceName} ID`,
|
|
3894
|
+
schema: { type: "string" }
|
|
3895
|
+
},
|
|
3896
|
+
...isToMany ? [
|
|
3897
|
+
{
|
|
3898
|
+
name: "limit",
|
|
3899
|
+
in: "query",
|
|
3900
|
+
description: "Maximum number of items to return",
|
|
3901
|
+
schema: { type: "integer", default: 100, minimum: 1, maximum: 1e3 }
|
|
3902
|
+
},
|
|
3903
|
+
{
|
|
3904
|
+
name: "offset",
|
|
3905
|
+
in: "query",
|
|
3906
|
+
description: "Number of items to skip",
|
|
3907
|
+
schema: { type: "integer", default: 0, minimum: 0 }
|
|
3908
|
+
}
|
|
3909
|
+
] : []
|
|
3910
|
+
],
|
|
3911
|
+
responses: {
|
|
3912
|
+
200: {
|
|
3913
|
+
description: "Successful response",
|
|
3914
|
+
content: {
|
|
3915
|
+
"application/json": {
|
|
3916
|
+
schema: isToMany ? {
|
|
3917
|
+
type: "object",
|
|
3918
|
+
properties: {
|
|
3919
|
+
success: { type: "boolean", example: true },
|
|
3920
|
+
data: {
|
|
3921
|
+
type: "array",
|
|
3922
|
+
items: relatedSchema
|
|
3923
|
+
},
|
|
3924
|
+
pagination: {
|
|
3925
|
+
type: "object",
|
|
3926
|
+
properties: {
|
|
3927
|
+
total: { type: "integer" },
|
|
3928
|
+
page: { type: "integer" },
|
|
3929
|
+
pageSize: { type: "integer" },
|
|
3930
|
+
pageCount: { type: "integer" }
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
} : {
|
|
3935
|
+
type: "object",
|
|
3936
|
+
properties: {
|
|
3937
|
+
success: { type: "boolean", example: true },
|
|
3938
|
+
data: relatedSchema
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
},
|
|
3943
|
+
...isToMany ? {
|
|
3944
|
+
headers: {
|
|
3945
|
+
"X-Total-Count": {
|
|
3946
|
+
description: "Total number of related records",
|
|
3947
|
+
schema: { type: "integer" }
|
|
3948
|
+
},
|
|
3949
|
+
"X-Page-Count": {
|
|
3950
|
+
description: "Total number of pages",
|
|
3951
|
+
schema: { type: "integer" }
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
} : {}
|
|
3955
|
+
},
|
|
3956
|
+
404: {
|
|
3957
|
+
description: `${resourceName} not found` + (isToMany ? "" : " or no related resource exists"),
|
|
3958
|
+
content: {
|
|
3959
|
+
"application/json": {
|
|
3960
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
};
|
|
3967
|
+
return paths;
|
|
3968
|
+
}
|
|
3969
|
+
function generateOpenAPISpec(database, config = {}) {
|
|
3970
|
+
const {
|
|
3971
|
+
title = "s3db.js API",
|
|
3972
|
+
version = "1.0.0",
|
|
3973
|
+
description = "Auto-generated REST API documentation for s3db.js resources",
|
|
3974
|
+
serverUrl = "http://localhost:3000",
|
|
3975
|
+
auth = {},
|
|
3976
|
+
resources: resourceConfigs = {}
|
|
3977
|
+
} = config;
|
|
3978
|
+
const resourcesTableRows = [];
|
|
3979
|
+
for (const [name, resource] of Object.entries(database.resources)) {
|
|
3980
|
+
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
3981
|
+
continue;
|
|
3982
|
+
}
|
|
3983
|
+
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
3984
|
+
const resourceDescription = resource.config?.description;
|
|
3985
|
+
const descText = typeof resourceDescription === "object" ? resourceDescription.resource : resourceDescription || "No description";
|
|
3986
|
+
resourcesTableRows.push(`| ${name} | ${descText} | \`/${version2}/${name}\` |`);
|
|
3987
|
+
}
|
|
3988
|
+
const enhancedDescription = `${description}
|
|
3989
|
+
|
|
3990
|
+
## Available Resources
|
|
3991
|
+
|
|
3992
|
+
| Resource | Description | Base Path |
|
|
3993
|
+
|----------|-------------|-----------|
|
|
3994
|
+
${resourcesTableRows.join("\n")}
|
|
3995
|
+
|
|
3996
|
+
---
|
|
3997
|
+
|
|
3998
|
+
For detailed information about each endpoint, see the sections below.`;
|
|
3999
|
+
const spec = {
|
|
4000
|
+
openapi: "3.1.0",
|
|
4001
|
+
info: {
|
|
4002
|
+
title,
|
|
4003
|
+
version,
|
|
4004
|
+
description: enhancedDescription,
|
|
4005
|
+
contact: {
|
|
4006
|
+
name: "s3db.js",
|
|
4007
|
+
url: "https://github.com/forattini-dev/s3db.js"
|
|
4008
|
+
}
|
|
4009
|
+
},
|
|
4010
|
+
servers: [
|
|
4011
|
+
{
|
|
4012
|
+
url: serverUrl,
|
|
4013
|
+
description: "API Server"
|
|
4014
|
+
}
|
|
4015
|
+
],
|
|
4016
|
+
paths: {},
|
|
4017
|
+
components: {
|
|
4018
|
+
schemas: {
|
|
4019
|
+
Error: {
|
|
4020
|
+
type: "object",
|
|
4021
|
+
properties: {
|
|
4022
|
+
success: { type: "boolean", example: false },
|
|
4023
|
+
error: {
|
|
4024
|
+
type: "object",
|
|
4025
|
+
properties: {
|
|
4026
|
+
message: { type: "string" },
|
|
4027
|
+
code: { type: "string" },
|
|
4028
|
+
details: { type: "object" }
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
},
|
|
4033
|
+
ValidationError: {
|
|
4034
|
+
type: "object",
|
|
4035
|
+
properties: {
|
|
4036
|
+
success: { type: "boolean", example: false },
|
|
4037
|
+
error: {
|
|
4038
|
+
type: "object",
|
|
4039
|
+
properties: {
|
|
4040
|
+
message: { type: "string", example: "Validation failed" },
|
|
4041
|
+
code: { type: "string", example: "VALIDATION_ERROR" },
|
|
4042
|
+
details: {
|
|
4043
|
+
type: "object",
|
|
4044
|
+
properties: {
|
|
4045
|
+
errors: {
|
|
4046
|
+
type: "array",
|
|
4047
|
+
items: {
|
|
4048
|
+
type: "object",
|
|
4049
|
+
properties: {
|
|
4050
|
+
field: { type: "string" },
|
|
4051
|
+
message: { type: "string" },
|
|
4052
|
+
expected: { type: "string" },
|
|
4053
|
+
actual: {}
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
},
|
|
4064
|
+
securitySchemes: {}
|
|
4065
|
+
},
|
|
4066
|
+
tags: []
|
|
4067
|
+
};
|
|
4068
|
+
if (auth.jwt?.enabled) {
|
|
4069
|
+
spec.components.securitySchemes.bearerAuth = {
|
|
4070
|
+
type: "http",
|
|
4071
|
+
scheme: "bearer",
|
|
4072
|
+
bearerFormat: "JWT",
|
|
4073
|
+
description: "JWT authentication"
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
if (auth.apiKey?.enabled) {
|
|
4077
|
+
spec.components.securitySchemes.apiKeyAuth = {
|
|
4078
|
+
type: "apiKey",
|
|
4079
|
+
in: "header",
|
|
4080
|
+
name: auth.apiKey.headerName || "X-API-Key",
|
|
4081
|
+
description: "API Key authentication"
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
if (auth.basic?.enabled) {
|
|
4085
|
+
spec.components.securitySchemes.basicAuth = {
|
|
4086
|
+
type: "http",
|
|
4087
|
+
scheme: "basic",
|
|
4088
|
+
description: "HTTP Basic authentication"
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
4091
|
+
const resources = database.resources;
|
|
4092
|
+
const relationsPlugin = database.plugins?.relation || database.plugins?.RelationPlugin || null;
|
|
4093
|
+
for (const [name, resource] of Object.entries(resources)) {
|
|
4094
|
+
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
4095
|
+
continue;
|
|
4096
|
+
}
|
|
4097
|
+
const config2 = resourceConfigs[name] || {
|
|
4098
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
4099
|
+
auth: false
|
|
4100
|
+
};
|
|
4101
|
+
const version2 = resource.config?.currentVersion || resource.version || "v1";
|
|
4102
|
+
const paths = generateResourcePaths(resource, version2, config2);
|
|
4103
|
+
Object.assign(spec.paths, paths);
|
|
4104
|
+
const resourceDescription = resource.config?.description;
|
|
4105
|
+
const tagDescription = typeof resourceDescription === "object" ? resourceDescription.resource : resourceDescription || `Operations for ${name} resource`;
|
|
4106
|
+
spec.tags.push({
|
|
4107
|
+
name,
|
|
4108
|
+
description: tagDescription
|
|
4109
|
+
});
|
|
4110
|
+
spec.components.schemas[name] = generateResourceSchema(resource);
|
|
4111
|
+
if (relationsPlugin && relationsPlugin.relations && relationsPlugin.relations[name]) {
|
|
4112
|
+
const relationsDef = relationsPlugin.relations[name];
|
|
4113
|
+
for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
|
|
4114
|
+
if (relationConfig.type === "belongsTo") {
|
|
4115
|
+
continue;
|
|
4116
|
+
}
|
|
4117
|
+
const exposeRelation = config2?.relations?.[relationName]?.expose !== false;
|
|
4118
|
+
if (!exposeRelation) {
|
|
4119
|
+
continue;
|
|
4120
|
+
}
|
|
4121
|
+
const relatedResource = database.resources[relationConfig.resource];
|
|
4122
|
+
if (!relatedResource) {
|
|
4123
|
+
continue;
|
|
4124
|
+
}
|
|
4125
|
+
const relatedSchema = generateResourceSchema(relatedResource);
|
|
4126
|
+
const relationalPaths = generateRelationalPaths(
|
|
4127
|
+
resource,
|
|
4128
|
+
relationName,
|
|
4129
|
+
relationConfig,
|
|
4130
|
+
version2,
|
|
4131
|
+
relatedSchema
|
|
4132
|
+
);
|
|
4133
|
+
Object.assign(spec.paths, relationalPaths);
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
if (auth.jwt?.enabled || auth.apiKey?.enabled || auth.basic?.enabled) {
|
|
4138
|
+
spec.paths["/auth/login"] = {
|
|
4139
|
+
post: {
|
|
4140
|
+
tags: ["Authentication"],
|
|
4141
|
+
summary: "Login",
|
|
4142
|
+
description: "Authenticate with username and password",
|
|
4143
|
+
requestBody: {
|
|
4144
|
+
required: true,
|
|
4145
|
+
content: {
|
|
4146
|
+
"application/json": {
|
|
4147
|
+
schema: {
|
|
4148
|
+
type: "object",
|
|
4149
|
+
properties: {
|
|
4150
|
+
username: { type: "string" },
|
|
4151
|
+
password: { type: "string", format: "password" }
|
|
4152
|
+
},
|
|
4153
|
+
required: ["username", "password"]
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
},
|
|
4158
|
+
responses: {
|
|
4159
|
+
200: {
|
|
4160
|
+
description: "Login successful",
|
|
4161
|
+
content: {
|
|
4162
|
+
"application/json": {
|
|
4163
|
+
schema: {
|
|
4164
|
+
type: "object",
|
|
4165
|
+
properties: {
|
|
4166
|
+
success: { type: "boolean", example: true },
|
|
4167
|
+
data: {
|
|
4168
|
+
type: "object",
|
|
4169
|
+
properties: {
|
|
4170
|
+
token: { type: "string" },
|
|
4171
|
+
user: { type: "object" }
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
},
|
|
4179
|
+
401: {
|
|
4180
|
+
description: "Invalid credentials",
|
|
4181
|
+
content: {
|
|
4182
|
+
"application/json": {
|
|
4183
|
+
schema: { $ref: "#/components/schemas/Error" }
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
};
|
|
4190
|
+
spec.paths["/auth/register"] = {
|
|
4191
|
+
post: {
|
|
4192
|
+
tags: ["Authentication"],
|
|
4193
|
+
summary: "Register",
|
|
4194
|
+
description: "Register a new user",
|
|
4195
|
+
requestBody: {
|
|
4196
|
+
required: true,
|
|
4197
|
+
content: {
|
|
4198
|
+
"application/json": {
|
|
4199
|
+
schema: {
|
|
4200
|
+
type: "object",
|
|
4201
|
+
properties: {
|
|
4202
|
+
username: { type: "string", minLength: 3 },
|
|
4203
|
+
password: { type: "string", format: "password", minLength: 8 },
|
|
4204
|
+
email: { type: "string", format: "email" }
|
|
4205
|
+
},
|
|
4206
|
+
required: ["username", "password"]
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
},
|
|
4211
|
+
responses: {
|
|
4212
|
+
201: {
|
|
4213
|
+
description: "User registered successfully",
|
|
4214
|
+
content: {
|
|
4215
|
+
"application/json": {
|
|
4216
|
+
schema: {
|
|
4217
|
+
type: "object",
|
|
4218
|
+
properties: {
|
|
4219
|
+
success: { type: "boolean", example: true },
|
|
4220
|
+
data: {
|
|
4221
|
+
type: "object",
|
|
4222
|
+
properties: {
|
|
4223
|
+
token: { type: "string" },
|
|
4224
|
+
user: { type: "object" }
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
};
|
|
4235
|
+
spec.tags.push({
|
|
4236
|
+
name: "Authentication",
|
|
4237
|
+
description: "Authentication endpoints"
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4240
|
+
spec.paths["/health"] = {
|
|
4241
|
+
get: {
|
|
4242
|
+
tags: ["Health"],
|
|
4243
|
+
summary: "Generic Health Check",
|
|
4244
|
+
description: "Generic health check endpoint that includes references to liveness and readiness probes",
|
|
4245
|
+
responses: {
|
|
4246
|
+
200: {
|
|
4247
|
+
description: "API is healthy",
|
|
4248
|
+
content: {
|
|
4249
|
+
"application/json": {
|
|
4250
|
+
schema: {
|
|
4251
|
+
type: "object",
|
|
4252
|
+
properties: {
|
|
4253
|
+
success: { type: "boolean", example: true },
|
|
4254
|
+
data: {
|
|
4255
|
+
type: "object",
|
|
4256
|
+
properties: {
|
|
4257
|
+
status: { type: "string", example: "ok" },
|
|
4258
|
+
uptime: { type: "number", description: "Process uptime in seconds" },
|
|
4259
|
+
timestamp: { type: "string", format: "date-time" },
|
|
4260
|
+
checks: {
|
|
4261
|
+
type: "object",
|
|
4262
|
+
properties: {
|
|
4263
|
+
liveness: { type: "string", example: "/health/live" },
|
|
4264
|
+
readiness: { type: "string", example: "/health/ready" }
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
};
|
|
4277
|
+
spec.paths["/health/live"] = {
|
|
4278
|
+
get: {
|
|
4279
|
+
tags: ["Health"],
|
|
4280
|
+
summary: "Liveness Probe",
|
|
4281
|
+
description: "Kubernetes liveness probe - checks if the application is alive. If this fails, Kubernetes will restart the pod.",
|
|
4282
|
+
responses: {
|
|
4283
|
+
200: {
|
|
4284
|
+
description: "Application is alive",
|
|
4285
|
+
content: {
|
|
4286
|
+
"application/json": {
|
|
4287
|
+
schema: {
|
|
4288
|
+
type: "object",
|
|
4289
|
+
properties: {
|
|
4290
|
+
success: { type: "boolean", example: true },
|
|
4291
|
+
data: {
|
|
4292
|
+
type: "object",
|
|
4293
|
+
properties: {
|
|
4294
|
+
status: { type: "string", example: "alive" },
|
|
4295
|
+
timestamp: { type: "string", format: "date-time" }
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
};
|
|
4306
|
+
spec.paths["/health/ready"] = {
|
|
4307
|
+
get: {
|
|
4308
|
+
tags: ["Health"],
|
|
4309
|
+
summary: "Readiness Probe",
|
|
4310
|
+
description: "Kubernetes readiness probe - checks if the application is ready to receive traffic. If this fails, Kubernetes will remove the pod from service endpoints.",
|
|
4311
|
+
responses: {
|
|
4312
|
+
200: {
|
|
4313
|
+
description: "Application is ready to receive traffic",
|
|
4314
|
+
content: {
|
|
4315
|
+
"application/json": {
|
|
4316
|
+
schema: {
|
|
4317
|
+
type: "object",
|
|
4318
|
+
properties: {
|
|
4319
|
+
success: { type: "boolean", example: true },
|
|
4320
|
+
data: {
|
|
4321
|
+
type: "object",
|
|
4322
|
+
properties: {
|
|
4323
|
+
status: { type: "string", example: "ready" },
|
|
4324
|
+
database: {
|
|
4325
|
+
type: "object",
|
|
4326
|
+
properties: {
|
|
4327
|
+
connected: { type: "boolean", example: true },
|
|
4328
|
+
resources: { type: "integer", example: 5 }
|
|
4329
|
+
}
|
|
4330
|
+
},
|
|
4331
|
+
timestamp: { type: "string", format: "date-time" }
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
},
|
|
4339
|
+
503: {
|
|
4340
|
+
description: "Application is not ready",
|
|
4341
|
+
content: {
|
|
4342
|
+
"application/json": {
|
|
4343
|
+
schema: {
|
|
4344
|
+
type: "object",
|
|
4345
|
+
properties: {
|
|
4346
|
+
success: { type: "boolean", example: false },
|
|
4347
|
+
error: {
|
|
4348
|
+
type: "object",
|
|
4349
|
+
properties: {
|
|
4350
|
+
message: { type: "string", example: "Service not ready" },
|
|
4351
|
+
code: { type: "string", example: "NOT_READY" },
|
|
4352
|
+
details: {
|
|
4353
|
+
type: "object",
|
|
4354
|
+
properties: {
|
|
4355
|
+
database: {
|
|
4356
|
+
type: "object",
|
|
4357
|
+
properties: {
|
|
4358
|
+
connected: { type: "boolean", example: false },
|
|
4359
|
+
resources: { type: "integer", example: 0 }
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4374
|
+
spec.tags.push({
|
|
4375
|
+
name: "Health",
|
|
4376
|
+
description: "Health check endpoints for monitoring and Kubernetes probes"
|
|
4377
|
+
});
|
|
4378
|
+
const metricsPlugin = database.plugins?.metrics || database.plugins?.MetricsPlugin;
|
|
4379
|
+
if (metricsPlugin && metricsPlugin.config?.prometheus?.enabled) {
|
|
4380
|
+
const metricsPath = metricsPlugin.config.prometheus.path || "/metrics";
|
|
4381
|
+
const isIntegrated = metricsPlugin.config.prometheus.mode !== "standalone";
|
|
4382
|
+
if (isIntegrated) {
|
|
4383
|
+
spec.paths[metricsPath] = {
|
|
4384
|
+
get: {
|
|
4385
|
+
tags: ["Monitoring"],
|
|
4386
|
+
summary: "Prometheus Metrics",
|
|
4387
|
+
description: "Exposes application metrics in Prometheus text-based exposition format for monitoring and observability. Metrics include operation counts, durations, errors, uptime, and resource statistics.",
|
|
4388
|
+
responses: {
|
|
4389
|
+
200: {
|
|
4390
|
+
description: "Metrics in Prometheus format",
|
|
4391
|
+
content: {
|
|
4392
|
+
"text/plain": {
|
|
4393
|
+
schema: {
|
|
4394
|
+
type: "string",
|
|
4395
|
+
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'
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
};
|
|
4403
|
+
spec.tags.push({
|
|
4404
|
+
name: "Monitoring",
|
|
4405
|
+
description: "Monitoring and observability endpoints (Prometheus)"
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
return spec;
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
class ApiServer {
|
|
4413
|
+
/**
|
|
4414
|
+
* Create API server
|
|
4415
|
+
* @param {Object} options - Server options
|
|
4416
|
+
* @param {number} options.port - Server port
|
|
4417
|
+
* @param {string} options.host - Server host
|
|
4418
|
+
* @param {Object} options.database - s3db.js database instance
|
|
4419
|
+
* @param {Object} options.resources - Resource configuration
|
|
4420
|
+
* @param {Array} options.middlewares - Global middlewares
|
|
4421
|
+
*/
|
|
4422
|
+
constructor(options = {}) {
|
|
4423
|
+
this.options = {
|
|
4424
|
+
port: options.port || 3e3,
|
|
4425
|
+
host: options.host || "0.0.0.0",
|
|
4426
|
+
database: options.database,
|
|
4427
|
+
resources: options.resources || {},
|
|
4428
|
+
middlewares: options.middlewares || [],
|
|
4429
|
+
verbose: options.verbose || false,
|
|
4430
|
+
auth: options.auth || {},
|
|
4431
|
+
docsEnabled: options.docsEnabled !== false,
|
|
4432
|
+
// Enable /docs by default
|
|
4433
|
+
docsUI: options.docsUI || "redoc",
|
|
4434
|
+
// 'swagger' or 'redoc'
|
|
4435
|
+
maxBodySize: options.maxBodySize || 10 * 1024 * 1024,
|
|
4436
|
+
// 10MB default
|
|
4437
|
+
rootHandler: options.rootHandler,
|
|
4438
|
+
// Custom handler for root path, if not provided redirects to /docs
|
|
4439
|
+
apiInfo: {
|
|
4440
|
+
title: options.apiTitle || "s3db.js API",
|
|
4441
|
+
version: options.apiVersion || "1.0.0",
|
|
4442
|
+
description: options.apiDescription || "Auto-generated REST API for s3db.js resources"
|
|
4443
|
+
}
|
|
4444
|
+
};
|
|
4445
|
+
this.app = null;
|
|
4446
|
+
this.server = null;
|
|
4447
|
+
this.isRunning = false;
|
|
4448
|
+
this.openAPISpec = null;
|
|
4449
|
+
this.initialized = false;
|
|
4450
|
+
this.relationsPlugin = this.options.database?.plugins?.relation || this.options.database?.plugins?.RelationPlugin || null;
|
|
4451
|
+
}
|
|
4452
|
+
/**
|
|
4453
|
+
* Setup all routes
|
|
4454
|
+
* @private
|
|
4455
|
+
*/
|
|
4456
|
+
_setupRoutes() {
|
|
4457
|
+
this.options.middlewares.forEach((middleware) => {
|
|
4458
|
+
this.app.use("*", middleware);
|
|
4459
|
+
});
|
|
4460
|
+
this.app.use("*", async (c, next) => {
|
|
4461
|
+
const method = c.req.method;
|
|
4462
|
+
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
4463
|
+
const contentLength = c.req.header("content-length");
|
|
4464
|
+
if (contentLength) {
|
|
4465
|
+
const size = parseInt(contentLength);
|
|
4466
|
+
if (size > this.options.maxBodySize) {
|
|
4467
|
+
const response = payloadTooLarge(size, this.options.maxBodySize);
|
|
4468
|
+
c.header("Connection", "close");
|
|
4469
|
+
return c.json(response, response._status);
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
await next();
|
|
4474
|
+
});
|
|
4475
|
+
this.app.get("/health/live", (c) => {
|
|
4476
|
+
const response = success({
|
|
4477
|
+
status: "alive",
|
|
4478
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4479
|
+
});
|
|
4480
|
+
return c.json(response);
|
|
4481
|
+
});
|
|
4482
|
+
this.app.get("/health/ready", (c) => {
|
|
4483
|
+
const isReady = this.options.database && this.options.database.connected && Object.keys(this.options.database.resources).length > 0;
|
|
4484
|
+
if (!isReady) {
|
|
4485
|
+
const response2 = error("Service not ready", {
|
|
4486
|
+
status: 503,
|
|
4487
|
+
code: "NOT_READY",
|
|
4488
|
+
details: {
|
|
4489
|
+
database: {
|
|
4490
|
+
connected: this.options.database?.connected || false,
|
|
4491
|
+
resources: Object.keys(this.options.database?.resources || {}).length
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
});
|
|
4495
|
+
return c.json(response2, 503);
|
|
4496
|
+
}
|
|
4497
|
+
const response = success({
|
|
4498
|
+
status: "ready",
|
|
4499
|
+
database: {
|
|
4500
|
+
connected: true,
|
|
4501
|
+
resources: Object.keys(this.options.database.resources).length
|
|
4502
|
+
},
|
|
4503
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4504
|
+
});
|
|
4505
|
+
return c.json(response);
|
|
4506
|
+
});
|
|
4507
|
+
this.app.get("/health", (c) => {
|
|
4508
|
+
const response = success({
|
|
4509
|
+
status: "ok",
|
|
4510
|
+
uptime: process.uptime(),
|
|
4511
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4512
|
+
checks: {
|
|
4513
|
+
liveness: "/health/live",
|
|
4514
|
+
readiness: "/health/ready"
|
|
4515
|
+
}
|
|
4516
|
+
});
|
|
4517
|
+
return c.json(response);
|
|
4518
|
+
});
|
|
4519
|
+
this.app.get("/", (c) => {
|
|
4520
|
+
if (this.options.rootHandler) {
|
|
4521
|
+
return this.options.rootHandler(c);
|
|
4522
|
+
}
|
|
4523
|
+
return c.redirect("/docs", 302);
|
|
4524
|
+
});
|
|
4525
|
+
if (this.options.docsEnabled) {
|
|
4526
|
+
this.app.get("/openapi.json", (c) => {
|
|
4527
|
+
if (!this.openAPISpec) {
|
|
4528
|
+
this.openAPISpec = this._generateOpenAPISpec();
|
|
4529
|
+
}
|
|
4530
|
+
return c.json(this.openAPISpec);
|
|
4531
|
+
});
|
|
4532
|
+
if (this.options.docsUI === "swagger") {
|
|
4533
|
+
this.app.get("/docs", this.swaggerUI({
|
|
4534
|
+
url: "/openapi.json"
|
|
4535
|
+
}));
|
|
4536
|
+
} else {
|
|
4537
|
+
this.app.get("/docs", (c) => {
|
|
4538
|
+
return c.html(`<!DOCTYPE html>
|
|
4539
|
+
<html lang="en">
|
|
4540
|
+
<head>
|
|
4541
|
+
<meta charset="UTF-8">
|
|
4542
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4543
|
+
<title>${this.options.apiInfo.title} - API Documentation</title>
|
|
4544
|
+
<style>
|
|
4545
|
+
body {
|
|
4546
|
+
margin: 0;
|
|
4547
|
+
padding: 0;
|
|
4548
|
+
}
|
|
4549
|
+
</style>
|
|
4550
|
+
</head>
|
|
4551
|
+
<body>
|
|
4552
|
+
<redoc spec-url="/openapi.json"></redoc>
|
|
4553
|
+
<script src="https://cdn.redoc.ly/redoc/v2.5.1/bundles/redoc.standalone.js"><\/script>
|
|
4554
|
+
</body>
|
|
4555
|
+
</html>`);
|
|
4556
|
+
});
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
this._setupResourceRoutes();
|
|
4560
|
+
if (this.relationsPlugin) {
|
|
4561
|
+
this._setupRelationalRoutes();
|
|
4562
|
+
}
|
|
4563
|
+
this.app.onError((err, c) => {
|
|
4564
|
+
return errorHandler(err, c);
|
|
4565
|
+
});
|
|
4566
|
+
this.app.notFound((c) => {
|
|
4567
|
+
const response = error("Route not found", {
|
|
4568
|
+
status: 404,
|
|
4569
|
+
code: "NOT_FOUND",
|
|
4570
|
+
details: {
|
|
4571
|
+
path: c.req.path,
|
|
4572
|
+
method: c.req.method
|
|
4573
|
+
}
|
|
4574
|
+
});
|
|
4575
|
+
return c.json(response, 404);
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Setup routes for all resources
|
|
4580
|
+
* @private
|
|
4581
|
+
*/
|
|
4582
|
+
_setupResourceRoutes() {
|
|
4583
|
+
const { database, resources: resourceConfigs } = this.options;
|
|
4584
|
+
const resources = database.resources;
|
|
4585
|
+
for (const [name, resource] of Object.entries(resources)) {
|
|
4586
|
+
if (name.startsWith("plg_") && !resourceConfigs[name]) {
|
|
4587
|
+
continue;
|
|
4588
|
+
}
|
|
4589
|
+
const config = resourceConfigs[name] || {
|
|
4590
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]};
|
|
4591
|
+
const version = resource.config?.currentVersion || resource.version || "v1";
|
|
4592
|
+
const resourceApp = createResourceRoutes(resource, version, {
|
|
4593
|
+
methods: config.methods,
|
|
4594
|
+
customMiddleware: config.customMiddleware || [],
|
|
4595
|
+
enableValidation: config.validation !== false
|
|
4596
|
+
}, this.Hono);
|
|
4597
|
+
this.app.route(`/${version}/${name}`, resourceApp);
|
|
4598
|
+
if (this.options.verbose) {
|
|
4599
|
+
console.log(`[API Plugin] Mounted routes for resource '${name}' at /${version}/${name}`);
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
/**
|
|
4604
|
+
* Setup relational routes (when RelationPlugin is active)
|
|
4605
|
+
* @private
|
|
4606
|
+
*/
|
|
4607
|
+
_setupRelationalRoutes() {
|
|
4608
|
+
if (!this.relationsPlugin || !this.relationsPlugin.relations) {
|
|
4609
|
+
return;
|
|
4610
|
+
}
|
|
4611
|
+
const { database } = this.options;
|
|
4612
|
+
const relations = this.relationsPlugin.relations;
|
|
4613
|
+
if (this.options.verbose) {
|
|
4614
|
+
console.log("[API Plugin] Setting up relational routes...");
|
|
4615
|
+
}
|
|
4616
|
+
for (const [resourceName, relationsDef] of Object.entries(relations)) {
|
|
4617
|
+
const resource = database.resources[resourceName];
|
|
4618
|
+
if (!resource) {
|
|
4619
|
+
if (this.options.verbose) {
|
|
4620
|
+
console.warn(`[API Plugin] Resource '${resourceName}' not found for relational routes`);
|
|
4621
|
+
}
|
|
4622
|
+
continue;
|
|
4623
|
+
}
|
|
4624
|
+
if (resourceName.startsWith("plg_") && !this.options.resources[resourceName]) {
|
|
4625
|
+
continue;
|
|
4626
|
+
}
|
|
4627
|
+
const version = resource.config?.currentVersion || resource.version || "v1";
|
|
4628
|
+
for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
|
|
4629
|
+
if (relationConfig.type === "belongsTo") {
|
|
4630
|
+
continue;
|
|
4631
|
+
}
|
|
4632
|
+
const resourceConfig = this.options.resources[resourceName];
|
|
4633
|
+
const exposeRelation = resourceConfig?.relations?.[relationName]?.expose !== false;
|
|
4634
|
+
if (!exposeRelation) {
|
|
4635
|
+
continue;
|
|
4636
|
+
}
|
|
4637
|
+
const relationalApp = createRelationalRoutes(
|
|
4638
|
+
resource,
|
|
4639
|
+
relationName,
|
|
4640
|
+
relationConfig,
|
|
4641
|
+
version,
|
|
4642
|
+
this.Hono
|
|
4643
|
+
);
|
|
4644
|
+
this.app.route(`/${version}/${resourceName}/:id/${relationName}`, relationalApp);
|
|
4645
|
+
if (this.options.verbose) {
|
|
4646
|
+
console.log(
|
|
4647
|
+
`[API Plugin] Mounted relational route: /${version}/${resourceName}/:id/${relationName} (${relationConfig.type} -> ${relationConfig.resource})`
|
|
4648
|
+
);
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
/**
|
|
4654
|
+
* Start the server
|
|
4655
|
+
* @returns {Promise<void>}
|
|
4656
|
+
*/
|
|
4657
|
+
async start() {
|
|
4658
|
+
if (this.isRunning) {
|
|
4659
|
+
console.warn("[API Plugin] Server is already running");
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
if (!this.initialized) {
|
|
4663
|
+
const { Hono } = await import('hono');
|
|
4664
|
+
const { serve } = await import('@hono/node-server');
|
|
4665
|
+
const { swaggerUI } = await import('@hono/swagger-ui');
|
|
4666
|
+
this.Hono = Hono;
|
|
4667
|
+
this.serve = serve;
|
|
4668
|
+
this.swaggerUI = swaggerUI;
|
|
4669
|
+
this.app = new Hono();
|
|
4670
|
+
this._setupRoutes();
|
|
4671
|
+
this.initialized = true;
|
|
4672
|
+
}
|
|
4673
|
+
const { port, host } = this.options;
|
|
4674
|
+
return new Promise((resolve, reject) => {
|
|
4675
|
+
try {
|
|
4676
|
+
this.server = this.serve({
|
|
4677
|
+
fetch: this.app.fetch,
|
|
4678
|
+
port,
|
|
4679
|
+
hostname: host
|
|
4680
|
+
}, (info) => {
|
|
4681
|
+
this.isRunning = true;
|
|
4682
|
+
console.log(`[API Plugin] Server listening on http://${info.address}:${info.port}`);
|
|
4683
|
+
resolve();
|
|
4684
|
+
});
|
|
4685
|
+
} catch (err) {
|
|
4686
|
+
reject(err);
|
|
4687
|
+
}
|
|
4688
|
+
});
|
|
4689
|
+
}
|
|
4690
|
+
/**
|
|
4691
|
+
* Stop the server
|
|
4692
|
+
* @returns {Promise<void>}
|
|
4693
|
+
*/
|
|
4694
|
+
async stop() {
|
|
4695
|
+
if (!this.isRunning) {
|
|
4696
|
+
console.warn("[API Plugin] Server is not running");
|
|
4697
|
+
return;
|
|
4698
|
+
}
|
|
4699
|
+
if (this.server && typeof this.server.close === "function") {
|
|
4700
|
+
await new Promise((resolve) => {
|
|
4701
|
+
this.server.close(() => {
|
|
4702
|
+
this.isRunning = false;
|
|
4703
|
+
console.log("[API Plugin] Server stopped");
|
|
4704
|
+
resolve();
|
|
4705
|
+
});
|
|
4706
|
+
});
|
|
4707
|
+
} else {
|
|
4708
|
+
this.isRunning = false;
|
|
4709
|
+
console.log("[API Plugin] Server stopped");
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
/**
|
|
4713
|
+
* Get server info
|
|
4714
|
+
* @returns {Object} Server information
|
|
4715
|
+
*/
|
|
4716
|
+
getInfo() {
|
|
4717
|
+
return {
|
|
4718
|
+
isRunning: this.isRunning,
|
|
4719
|
+
port: this.options.port,
|
|
4720
|
+
host: this.options.host,
|
|
4721
|
+
resources: Object.keys(this.options.database.resources).length
|
|
4722
|
+
};
|
|
4723
|
+
}
|
|
4724
|
+
/**
|
|
4725
|
+
* Get Hono app instance
|
|
4726
|
+
* @returns {Hono} Hono app
|
|
4727
|
+
*/
|
|
4728
|
+
getApp() {
|
|
4729
|
+
return this.app;
|
|
4730
|
+
}
|
|
4731
|
+
/**
|
|
4732
|
+
* Generate OpenAPI specification
|
|
4733
|
+
* @private
|
|
4734
|
+
* @returns {Object} OpenAPI spec
|
|
4735
|
+
*/
|
|
4736
|
+
_generateOpenAPISpec() {
|
|
4737
|
+
const { port, host, database, resources, auth, apiInfo } = this.options;
|
|
4738
|
+
return generateOpenAPISpec(database, {
|
|
4739
|
+
title: apiInfo.title,
|
|
4740
|
+
version: apiInfo.version,
|
|
4741
|
+
description: apiInfo.description,
|
|
4742
|
+
serverUrl: `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`,
|
|
4743
|
+
auth,
|
|
4744
|
+
resources
|
|
4745
|
+
});
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
|
|
2706
4749
|
class ApiPlugin extends Plugin {
|
|
2707
4750
|
/**
|
|
2708
4751
|
* Create API Plugin instance
|
|
@@ -3025,11 +5068,6 @@ class ApiPlugin extends Plugin {
|
|
|
3025
5068
|
if (this.config.verbose) {
|
|
3026
5069
|
console.log("[API Plugin] Starting server...");
|
|
3027
5070
|
}
|
|
3028
|
-
const serverPath = "./server.js";
|
|
3029
|
-
const { ApiServer } = await import(
|
|
3030
|
-
/* @vite-ignore */
|
|
3031
|
-
serverPath
|
|
3032
|
-
);
|
|
3033
5071
|
this.server = new ApiServer({
|
|
3034
5072
|
port: this.config.port,
|
|
3035
5073
|
host: this.config.host,
|
|
@@ -4590,7 +6628,7 @@ class BackupPlugin extends Plugin {
|
|
|
4590
6628
|
const storageInfo = this.driver.getStorageInfo();
|
|
4591
6629
|
console.log(`[BackupPlugin] Initialized with driver: ${storageInfo.type}`);
|
|
4592
6630
|
}
|
|
4593
|
-
this.emit("initialized", {
|
|
6631
|
+
this.emit("db:plugin:initialized", {
|
|
4594
6632
|
driver: this.driver.getType(),
|
|
4595
6633
|
config: this.driver.getStorageInfo()
|
|
4596
6634
|
});
|
|
@@ -4638,7 +6676,7 @@ class BackupPlugin extends Plugin {
|
|
|
4638
6676
|
if (this.config.onBackupStart) {
|
|
4639
6677
|
await this._executeHook(this.config.onBackupStart, type, { backupId });
|
|
4640
6678
|
}
|
|
4641
|
-
this.emit("
|
|
6679
|
+
this.emit("plg:backup:start", { id: backupId, type });
|
|
4642
6680
|
const metadata = await this._createBackupMetadata(backupId, type);
|
|
4643
6681
|
const tempBackupDir = path$1.join(this.config.tempDir, backupId);
|
|
4644
6682
|
await promises.mkdir(tempBackupDir, { recursive: true });
|
|
@@ -4671,7 +6709,7 @@ class BackupPlugin extends Plugin {
|
|
|
4671
6709
|
const stats = { backupId, type, size: totalSize, duration, driverInfo: uploadResult };
|
|
4672
6710
|
await this._executeHook(this.config.onBackupComplete, type, stats);
|
|
4673
6711
|
}
|
|
4674
|
-
this.emit("
|
|
6712
|
+
this.emit("plg:backup:complete", {
|
|
4675
6713
|
id: backupId,
|
|
4676
6714
|
type,
|
|
4677
6715
|
size: totalSize,
|
|
@@ -4699,7 +6737,7 @@ class BackupPlugin extends Plugin {
|
|
|
4699
6737
|
error: error.message,
|
|
4700
6738
|
duration: Date.now() - startTime
|
|
4701
6739
|
});
|
|
4702
|
-
this.emit("
|
|
6740
|
+
this.emit("plg:backup:error", { id: backupId, type, error: error.message });
|
|
4703
6741
|
throw error;
|
|
4704
6742
|
} finally {
|
|
4705
6743
|
this.activeBackups.delete(backupId);
|
|
@@ -4920,7 +6958,7 @@ class BackupPlugin extends Plugin {
|
|
|
4920
6958
|
if (this.config.onRestoreStart) {
|
|
4921
6959
|
await this._executeHook(this.config.onRestoreStart, backupId, options);
|
|
4922
6960
|
}
|
|
4923
|
-
this.emit("
|
|
6961
|
+
this.emit("plg:backup:restore-start", { id: backupId, options });
|
|
4924
6962
|
const backup = await this.getBackupStatus(backupId);
|
|
4925
6963
|
if (!backup) {
|
|
4926
6964
|
throw new Error(`Backup '${backupId}' not found`);
|
|
@@ -4943,7 +6981,7 @@ class BackupPlugin extends Plugin {
|
|
|
4943
6981
|
if (this.config.onRestoreComplete) {
|
|
4944
6982
|
await this._executeHook(this.config.onRestoreComplete, backupId, { restored: restoredResources });
|
|
4945
6983
|
}
|
|
4946
|
-
this.emit("
|
|
6984
|
+
this.emit("plg:backup:restore-complete", {
|
|
4947
6985
|
id: backupId,
|
|
4948
6986
|
restored: restoredResources
|
|
4949
6987
|
});
|
|
@@ -4958,7 +6996,7 @@ class BackupPlugin extends Plugin {
|
|
|
4958
6996
|
if (this.config.onRestoreError) {
|
|
4959
6997
|
await this._executeHook(this.config.onRestoreError, backupId, { error });
|
|
4960
6998
|
}
|
|
4961
|
-
this.emit("
|
|
6999
|
+
this.emit("plg:backup:restore-error", { id: backupId, error: error.message });
|
|
4962
7000
|
throw error;
|
|
4963
7001
|
}
|
|
4964
7002
|
}
|
|
@@ -5208,7 +7246,7 @@ class BackupPlugin extends Plugin {
|
|
|
5208
7246
|
}
|
|
5209
7247
|
async stop() {
|
|
5210
7248
|
for (const backupId of this.activeBackups) {
|
|
5211
|
-
this.emit("
|
|
7249
|
+
this.emit("plg:backup:cancelled", { id: backupId });
|
|
5212
7250
|
}
|
|
5213
7251
|
this.activeBackups.clear();
|
|
5214
7252
|
if (this.driver) {
|
|
@@ -6732,7 +8770,7 @@ class CachePlugin extends Plugin {
|
|
|
6732
8770
|
const specificKey = await this.generateCacheKey(resource, method, { id: data.id });
|
|
6733
8771
|
const [ok2, err2] = await this.clearCacheWithRetry(resource.cache, specificKey);
|
|
6734
8772
|
if (!ok2) {
|
|
6735
|
-
this.emit("
|
|
8773
|
+
this.emit("plg:cache:clear-error", {
|
|
6736
8774
|
resource: resource.name,
|
|
6737
8775
|
method,
|
|
6738
8776
|
id: data.id,
|
|
@@ -6750,7 +8788,7 @@ class CachePlugin extends Plugin {
|
|
|
6750
8788
|
const partitionKeyPrefix = path$1.join(keyPrefix, `partition=${partitionName}`);
|
|
6751
8789
|
const [ok2, err2] = await this.clearCacheWithRetry(resource.cache, partitionKeyPrefix);
|
|
6752
8790
|
if (!ok2) {
|
|
6753
|
-
this.emit("
|
|
8791
|
+
this.emit("plg:cache:clear-error", {
|
|
6754
8792
|
resource: resource.name,
|
|
6755
8793
|
partition: partitionName,
|
|
6756
8794
|
error: err2.message
|
|
@@ -6765,7 +8803,7 @@ class CachePlugin extends Plugin {
|
|
|
6765
8803
|
}
|
|
6766
8804
|
const [ok, err] = await this.clearCacheWithRetry(resource.cache, keyPrefix);
|
|
6767
8805
|
if (!ok) {
|
|
6768
|
-
this.emit("
|
|
8806
|
+
this.emit("plg:cache:clear-error", {
|
|
6769
8807
|
resource: resource.name,
|
|
6770
8808
|
type: "broad",
|
|
6771
8809
|
error: err.message
|
|
@@ -10665,7 +12703,7 @@ class GeoPlugin extends Plugin {
|
|
|
10665
12703
|
if (this.verbose) {
|
|
10666
12704
|
console.log(`[GeoPlugin] Installed with ${Object.keys(this.resources).length} resources`);
|
|
10667
12705
|
}
|
|
10668
|
-
this.emit("installed", {
|
|
12706
|
+
this.emit("db:plugin:installed", {
|
|
10669
12707
|
plugin: "GeoPlugin",
|
|
10670
12708
|
resources: Object.keys(this.resources)
|
|
10671
12709
|
});
|
|
@@ -11232,7 +13270,7 @@ class GeoPlugin extends Plugin {
|
|
|
11232
13270
|
if (this.verbose) {
|
|
11233
13271
|
console.log("[GeoPlugin] Uninstalled");
|
|
11234
13272
|
}
|
|
11235
|
-
this.emit("uninstalled", {
|
|
13273
|
+
this.emit("db:plugin:uninstalled", {
|
|
11236
13274
|
plugin: "GeoPlugin"
|
|
11237
13275
|
});
|
|
11238
13276
|
await super.uninstall();
|
|
@@ -13227,10 +15265,17 @@ class MLPlugin extends Plugin {
|
|
|
13227
15265
|
this.config = {
|
|
13228
15266
|
models: options.models || {},
|
|
13229
15267
|
verbose: options.verbose || false,
|
|
13230
|
-
minTrainingSamples: options.minTrainingSamples || 10
|
|
15268
|
+
minTrainingSamples: options.minTrainingSamples || 10,
|
|
15269
|
+
saveModel: options.saveModel !== false,
|
|
15270
|
+
// Default true
|
|
15271
|
+
saveTrainingData: options.saveTrainingData || false,
|
|
15272
|
+
enableVersioning: options.enableVersioning !== false
|
|
15273
|
+
// Default true
|
|
13231
15274
|
};
|
|
13232
|
-
requirePluginDependency("
|
|
15275
|
+
requirePluginDependency("ml-plugin");
|
|
13233
15276
|
this.models = {};
|
|
15277
|
+
this.modelVersions = /* @__PURE__ */ new Map();
|
|
15278
|
+
this.modelCache = /* @__PURE__ */ new Map();
|
|
13234
15279
|
this.training = /* @__PURE__ */ new Map();
|
|
13235
15280
|
this.insertCounters = /* @__PURE__ */ new Map();
|
|
13236
15281
|
this.intervals = [];
|
|
@@ -13254,6 +15299,8 @@ class MLPlugin extends Plugin {
|
|
|
13254
15299
|
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
13255
15300
|
await this._initializeModel(modelName, modelConfig);
|
|
13256
15301
|
}
|
|
15302
|
+
this._buildModelCache();
|
|
15303
|
+
this._injectResourceMethods();
|
|
13257
15304
|
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
13258
15305
|
if (modelConfig.autoTrain) {
|
|
13259
15306
|
this._setupAutoTraining(modelName, modelConfig);
|
|
@@ -13263,7 +15310,7 @@ class MLPlugin extends Plugin {
|
|
|
13263
15310
|
if (this.config.verbose) {
|
|
13264
15311
|
console.log(`[MLPlugin] Installed with ${Object.keys(this.models).length} models`);
|
|
13265
15312
|
}
|
|
13266
|
-
this.emit("installed", {
|
|
15313
|
+
this.emit("db:plugin:installed", {
|
|
13267
15314
|
plugin: "MLPlugin",
|
|
13268
15315
|
models: Object.keys(this.models)
|
|
13269
15316
|
});
|
|
@@ -13272,6 +15319,11 @@ class MLPlugin extends Plugin {
|
|
|
13272
15319
|
* Start the plugin
|
|
13273
15320
|
*/
|
|
13274
15321
|
async onStart() {
|
|
15322
|
+
if (this.config.enableVersioning) {
|
|
15323
|
+
for (const modelName of Object.keys(this.models)) {
|
|
15324
|
+
await this._initializeVersioning(modelName);
|
|
15325
|
+
}
|
|
15326
|
+
}
|
|
13275
15327
|
for (const modelName of Object.keys(this.models)) {
|
|
13276
15328
|
await this._loadModel(modelName);
|
|
13277
15329
|
}
|
|
@@ -13304,12 +15356,135 @@ class MLPlugin extends Plugin {
|
|
|
13304
15356
|
if (options.purgeData) {
|
|
13305
15357
|
for (const modelName of Object.keys(this.models)) {
|
|
13306
15358
|
await this._deleteModel(modelName);
|
|
15359
|
+
await this._deleteTrainingData(modelName);
|
|
15360
|
+
}
|
|
15361
|
+
if (this.config.verbose) {
|
|
15362
|
+
console.log("[MLPlugin] Purged all model data and training data");
|
|
13307
15363
|
}
|
|
15364
|
+
}
|
|
15365
|
+
}
|
|
15366
|
+
/**
|
|
15367
|
+
* Build model cache for fast lookup
|
|
15368
|
+
* @private
|
|
15369
|
+
*/
|
|
15370
|
+
_buildModelCache() {
|
|
15371
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
15372
|
+
const cacheKey = `${modelConfig.resource}_${modelConfig.target}`;
|
|
15373
|
+
this.modelCache.set(cacheKey, modelName);
|
|
13308
15374
|
if (this.config.verbose) {
|
|
13309
|
-
console.log(
|
|
15375
|
+
console.log(`[MLPlugin] Cached model "${modelName}" for ${modelConfig.resource}.predict(..., '${modelConfig.target}')`);
|
|
13310
15376
|
}
|
|
13311
15377
|
}
|
|
13312
15378
|
}
|
|
15379
|
+
/**
|
|
15380
|
+
* Inject ML methods into Resource instances
|
|
15381
|
+
* @private
|
|
15382
|
+
*/
|
|
15383
|
+
_injectResourceMethods() {
|
|
15384
|
+
if (!this.database._mlPlugin) {
|
|
15385
|
+
this.database._mlPlugin = this;
|
|
15386
|
+
}
|
|
15387
|
+
if (!this.database.Resource.prototype.predict) {
|
|
15388
|
+
this.database.Resource.prototype.predict = async function(input, targetAttribute) {
|
|
15389
|
+
const mlPlugin = this.database._mlPlugin;
|
|
15390
|
+
if (!mlPlugin) {
|
|
15391
|
+
throw new Error("MLPlugin not installed");
|
|
15392
|
+
}
|
|
15393
|
+
return await mlPlugin._resourcePredict(this.name, input, targetAttribute);
|
|
15394
|
+
};
|
|
15395
|
+
}
|
|
15396
|
+
if (!this.database.Resource.prototype.trainModel) {
|
|
15397
|
+
this.database.Resource.prototype.trainModel = async function(targetAttribute, options = {}) {
|
|
15398
|
+
const mlPlugin = this.database._mlPlugin;
|
|
15399
|
+
if (!mlPlugin) {
|
|
15400
|
+
throw new Error("MLPlugin not installed");
|
|
15401
|
+
}
|
|
15402
|
+
return await mlPlugin._resourceTrainModel(this.name, targetAttribute, options);
|
|
15403
|
+
};
|
|
15404
|
+
}
|
|
15405
|
+
if (!this.database.Resource.prototype.listModels) {
|
|
15406
|
+
this.database.Resource.prototype.listModels = function() {
|
|
15407
|
+
const mlPlugin = this.database._mlPlugin;
|
|
15408
|
+
if (!mlPlugin) {
|
|
15409
|
+
throw new Error("MLPlugin not installed");
|
|
15410
|
+
}
|
|
15411
|
+
return mlPlugin._resourceListModels(this.name);
|
|
15412
|
+
};
|
|
15413
|
+
}
|
|
15414
|
+
if (this.config.verbose) {
|
|
15415
|
+
console.log("[MLPlugin] Injected ML methods into Resource prototype");
|
|
15416
|
+
}
|
|
15417
|
+
}
|
|
15418
|
+
/**
|
|
15419
|
+
* Find model for a resource and target attribute
|
|
15420
|
+
* @private
|
|
15421
|
+
*/
|
|
15422
|
+
_findModelForResource(resourceName, targetAttribute) {
|
|
15423
|
+
const cacheKey = `${resourceName}_${targetAttribute}`;
|
|
15424
|
+
if (this.modelCache.has(cacheKey)) {
|
|
15425
|
+
return this.modelCache.get(cacheKey);
|
|
15426
|
+
}
|
|
15427
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
15428
|
+
if (modelConfig.resource === resourceName && modelConfig.target === targetAttribute) {
|
|
15429
|
+
this.modelCache.set(cacheKey, modelName);
|
|
15430
|
+
return modelName;
|
|
15431
|
+
}
|
|
15432
|
+
}
|
|
15433
|
+
return null;
|
|
15434
|
+
}
|
|
15435
|
+
/**
|
|
15436
|
+
* Resource predict implementation
|
|
15437
|
+
* @private
|
|
15438
|
+
*/
|
|
15439
|
+
async _resourcePredict(resourceName, input, targetAttribute) {
|
|
15440
|
+
const modelName = this._findModelForResource(resourceName, targetAttribute);
|
|
15441
|
+
if (!modelName) {
|
|
15442
|
+
throw new ModelNotFoundError(
|
|
15443
|
+
`No model found for resource "${resourceName}" with target "${targetAttribute}"`,
|
|
15444
|
+
{ resourceName, targetAttribute, availableModels: Object.keys(this.models) }
|
|
15445
|
+
);
|
|
15446
|
+
}
|
|
15447
|
+
if (this.config.verbose) {
|
|
15448
|
+
console.log(`[MLPlugin] Resource prediction: ${resourceName}.predict(..., '${targetAttribute}') -> model "${modelName}"`);
|
|
15449
|
+
}
|
|
15450
|
+
return await this.predict(modelName, input);
|
|
15451
|
+
}
|
|
15452
|
+
/**
|
|
15453
|
+
* Resource trainModel implementation
|
|
15454
|
+
* @private
|
|
15455
|
+
*/
|
|
15456
|
+
async _resourceTrainModel(resourceName, targetAttribute, options = {}) {
|
|
15457
|
+
const modelName = this._findModelForResource(resourceName, targetAttribute);
|
|
15458
|
+
if (!modelName) {
|
|
15459
|
+
throw new ModelNotFoundError(
|
|
15460
|
+
`No model found for resource "${resourceName}" with target "${targetAttribute}"`,
|
|
15461
|
+
{ resourceName, targetAttribute, availableModels: Object.keys(this.models) }
|
|
15462
|
+
);
|
|
15463
|
+
}
|
|
15464
|
+
if (this.config.verbose) {
|
|
15465
|
+
console.log(`[MLPlugin] Resource training: ${resourceName}.trainModel('${targetAttribute}') -> model "${modelName}"`);
|
|
15466
|
+
}
|
|
15467
|
+
return await this.train(modelName, options);
|
|
15468
|
+
}
|
|
15469
|
+
/**
|
|
15470
|
+
* List models for a resource
|
|
15471
|
+
* @private
|
|
15472
|
+
*/
|
|
15473
|
+
_resourceListModels(resourceName) {
|
|
15474
|
+
const models = [];
|
|
15475
|
+
for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
|
|
15476
|
+
if (modelConfig.resource === resourceName) {
|
|
15477
|
+
models.push({
|
|
15478
|
+
name: modelName,
|
|
15479
|
+
type: modelConfig.type,
|
|
15480
|
+
target: modelConfig.target,
|
|
15481
|
+
features: modelConfig.features,
|
|
15482
|
+
isTrained: this.models[modelName]?.isTrained || false
|
|
15483
|
+
});
|
|
15484
|
+
}
|
|
15485
|
+
}
|
|
15486
|
+
return models;
|
|
15487
|
+
}
|
|
13313
15488
|
/**
|
|
13314
15489
|
* Validate model configuration
|
|
13315
15490
|
* @private
|
|
@@ -13460,12 +15635,47 @@ class MLPlugin extends Plugin {
|
|
|
13460
15635
|
if (this.config.verbose) {
|
|
13461
15636
|
console.log(`[MLPlugin] Fetching training data for "${modelName}"...`);
|
|
13462
15637
|
}
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
15638
|
+
let data;
|
|
15639
|
+
const partition = modelConfig.partition;
|
|
15640
|
+
if (partition && partition.name) {
|
|
15641
|
+
if (this.config.verbose) {
|
|
15642
|
+
console.log(`[MLPlugin] Using partition "${partition.name}" with values:`, partition.values);
|
|
15643
|
+
}
|
|
15644
|
+
const [ok, err, partitionData] = await tryFn(
|
|
15645
|
+
() => resource.listPartition(partition.name, partition.values)
|
|
13468
15646
|
);
|
|
15647
|
+
if (!ok) {
|
|
15648
|
+
throw new TrainingError(
|
|
15649
|
+
`Failed to fetch training data from partition: ${err.message}`,
|
|
15650
|
+
{ modelName, resource: modelConfig.resource, partition: partition.name, originalError: err.message }
|
|
15651
|
+
);
|
|
15652
|
+
}
|
|
15653
|
+
data = partitionData;
|
|
15654
|
+
} else {
|
|
15655
|
+
const [ok, err, allData] = await tryFn(() => resource.list());
|
|
15656
|
+
if (!ok) {
|
|
15657
|
+
throw new TrainingError(
|
|
15658
|
+
`Failed to fetch training data: ${err.message}`,
|
|
15659
|
+
{ modelName, resource: modelConfig.resource, originalError: err.message }
|
|
15660
|
+
);
|
|
15661
|
+
}
|
|
15662
|
+
data = allData;
|
|
15663
|
+
}
|
|
15664
|
+
if (modelConfig.filter && typeof modelConfig.filter === "function") {
|
|
15665
|
+
if (this.config.verbose) {
|
|
15666
|
+
console.log(`[MLPlugin] Applying custom filter function...`);
|
|
15667
|
+
}
|
|
15668
|
+
const originalLength = data.length;
|
|
15669
|
+
data = data.filter(modelConfig.filter);
|
|
15670
|
+
if (this.config.verbose) {
|
|
15671
|
+
console.log(`[MLPlugin] Filter reduced dataset from ${originalLength} to ${data.length} samples`);
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15674
|
+
if (modelConfig.map && typeof modelConfig.map === "function") {
|
|
15675
|
+
if (this.config.verbose) {
|
|
15676
|
+
console.log(`[MLPlugin] Applying custom map function...`);
|
|
15677
|
+
}
|
|
15678
|
+
data = data.map(modelConfig.map);
|
|
13469
15679
|
}
|
|
13470
15680
|
if (!data || data.length < this.config.minTrainingSamples) {
|
|
13471
15681
|
throw new TrainingError(
|
|
@@ -13476,13 +15686,20 @@ class MLPlugin extends Plugin {
|
|
|
13476
15686
|
if (this.config.verbose) {
|
|
13477
15687
|
console.log(`[MLPlugin] Training "${modelName}" with ${data.length} samples...`);
|
|
13478
15688
|
}
|
|
15689
|
+
const shouldSaveTrainingData = modelConfig.saveTrainingData !== void 0 ? modelConfig.saveTrainingData : this.config.saveTrainingData;
|
|
15690
|
+
if (shouldSaveTrainingData) {
|
|
15691
|
+
await this._saveTrainingData(modelName, data);
|
|
15692
|
+
}
|
|
13479
15693
|
const result = await model.train(data);
|
|
13480
|
-
|
|
15694
|
+
const shouldSaveModel = modelConfig.saveModel !== void 0 ? modelConfig.saveModel : this.config.saveModel;
|
|
15695
|
+
if (shouldSaveModel) {
|
|
15696
|
+
await this._saveModel(modelName);
|
|
15697
|
+
}
|
|
13481
15698
|
this.stats.totalTrainings++;
|
|
13482
15699
|
if (this.config.verbose) {
|
|
13483
15700
|
console.log(`[MLPlugin] Training completed for "${modelName}":`, result);
|
|
13484
15701
|
}
|
|
13485
|
-
this.emit("
|
|
15702
|
+
this.emit("plg:ml:model-trained", {
|
|
13486
15703
|
modelName,
|
|
13487
15704
|
type: modelConfig.type,
|
|
13488
15705
|
result
|
|
@@ -13518,7 +15735,7 @@ class MLPlugin extends Plugin {
|
|
|
13518
15735
|
try {
|
|
13519
15736
|
const result = await model.predict(input);
|
|
13520
15737
|
this.stats.totalPredictions++;
|
|
13521
|
-
this.emit("prediction", {
|
|
15738
|
+
this.emit("plg:ml:prediction", {
|
|
13522
15739
|
modelName,
|
|
13523
15740
|
input,
|
|
13524
15741
|
result
|
|
@@ -13626,6 +15843,79 @@ class MLPlugin extends Plugin {
|
|
|
13626
15843
|
console.log(`[MLPlugin] Imported model "${modelName}"`);
|
|
13627
15844
|
}
|
|
13628
15845
|
}
|
|
15846
|
+
/**
|
|
15847
|
+
* Initialize versioning for a model
|
|
15848
|
+
* @private
|
|
15849
|
+
*/
|
|
15850
|
+
async _initializeVersioning(modelName) {
|
|
15851
|
+
try {
|
|
15852
|
+
const storage = this.getStorage();
|
|
15853
|
+
const modelConfig = this.config.models[modelName];
|
|
15854
|
+
const resourceName = modelConfig.resource;
|
|
15855
|
+
const [ok, err, versionInfo] = await tryFn(
|
|
15856
|
+
() => storage.get(storage.getPluginKey(resourceName, "metadata", modelName, "versions"))
|
|
15857
|
+
);
|
|
15858
|
+
if (ok && versionInfo) {
|
|
15859
|
+
this.modelVersions.set(modelName, {
|
|
15860
|
+
currentVersion: versionInfo.currentVersion || 1,
|
|
15861
|
+
latestVersion: versionInfo.latestVersion || 1
|
|
15862
|
+
});
|
|
15863
|
+
if (this.config.verbose) {
|
|
15864
|
+
console.log(`[MLPlugin] Loaded version info for "${modelName}": v${versionInfo.currentVersion}`);
|
|
15865
|
+
}
|
|
15866
|
+
} else {
|
|
15867
|
+
this.modelVersions.set(modelName, {
|
|
15868
|
+
currentVersion: 1,
|
|
15869
|
+
latestVersion: 0
|
|
15870
|
+
// No versions yet
|
|
15871
|
+
});
|
|
15872
|
+
if (this.config.verbose) {
|
|
15873
|
+
console.log(`[MLPlugin] Initialized versioning for "${modelName}"`);
|
|
15874
|
+
}
|
|
15875
|
+
}
|
|
15876
|
+
} catch (error) {
|
|
15877
|
+
console.error(`[MLPlugin] Failed to initialize versioning for "${modelName}":`, error.message);
|
|
15878
|
+
this.modelVersions.set(modelName, { currentVersion: 1, latestVersion: 0 });
|
|
15879
|
+
}
|
|
15880
|
+
}
|
|
15881
|
+
/**
|
|
15882
|
+
* Get next version number for a model
|
|
15883
|
+
* @private
|
|
15884
|
+
*/
|
|
15885
|
+
_getNextVersion(modelName) {
|
|
15886
|
+
const versionInfo = this.modelVersions.get(modelName) || { latestVersion: 0 };
|
|
15887
|
+
return versionInfo.latestVersion + 1;
|
|
15888
|
+
}
|
|
15889
|
+
/**
|
|
15890
|
+
* Update version info in storage
|
|
15891
|
+
* @private
|
|
15892
|
+
*/
|
|
15893
|
+
async _updateVersionInfo(modelName, version) {
|
|
15894
|
+
try {
|
|
15895
|
+
const storage = this.getStorage();
|
|
15896
|
+
const modelConfig = this.config.models[modelName];
|
|
15897
|
+
const resourceName = modelConfig.resource;
|
|
15898
|
+
const versionInfo = this.modelVersions.get(modelName) || { currentVersion: 1, latestVersion: 0 };
|
|
15899
|
+
versionInfo.latestVersion = Math.max(versionInfo.latestVersion, version);
|
|
15900
|
+
versionInfo.currentVersion = version;
|
|
15901
|
+
this.modelVersions.set(modelName, versionInfo);
|
|
15902
|
+
await storage.set(
|
|
15903
|
+
storage.getPluginKey(resourceName, "metadata", modelName, "versions"),
|
|
15904
|
+
{
|
|
15905
|
+
modelName,
|
|
15906
|
+
currentVersion: versionInfo.currentVersion,
|
|
15907
|
+
latestVersion: versionInfo.latestVersion,
|
|
15908
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15909
|
+
},
|
|
15910
|
+
{ behavior: "body-overflow" }
|
|
15911
|
+
);
|
|
15912
|
+
if (this.config.verbose) {
|
|
15913
|
+
console.log(`[MLPlugin] Updated version info for "${modelName}": current=v${versionInfo.currentVersion}, latest=v${versionInfo.latestVersion}`);
|
|
15914
|
+
}
|
|
15915
|
+
} catch (error) {
|
|
15916
|
+
console.error(`[MLPlugin] Failed to update version info for "${modelName}":`, error.message);
|
|
15917
|
+
}
|
|
15918
|
+
}
|
|
13629
15919
|
/**
|
|
13630
15920
|
* Save model to plugin storage
|
|
13631
15921
|
* @private
|
|
@@ -13633,6 +15923,8 @@ class MLPlugin extends Plugin {
|
|
|
13633
15923
|
async _saveModel(modelName) {
|
|
13634
15924
|
try {
|
|
13635
15925
|
const storage = this.getStorage();
|
|
15926
|
+
const modelConfig = this.config.models[modelName];
|
|
15927
|
+
const resourceName = modelConfig.resource;
|
|
13636
15928
|
const exportedModel = await this.models[modelName].export();
|
|
13637
15929
|
if (!exportedModel) {
|
|
13638
15930
|
if (this.config.verbose) {
|
|
@@ -13640,18 +15932,177 @@ class MLPlugin extends Plugin {
|
|
|
13640
15932
|
}
|
|
13641
15933
|
return;
|
|
13642
15934
|
}
|
|
13643
|
-
|
|
13644
|
-
|
|
13645
|
-
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
|
|
15935
|
+
const enableVersioning = this.config.enableVersioning;
|
|
15936
|
+
if (enableVersioning) {
|
|
15937
|
+
const version = this._getNextVersion(modelName);
|
|
15938
|
+
const modelStats = this.models[modelName].getStats();
|
|
15939
|
+
await storage.set(
|
|
15940
|
+
storage.getPluginKey(resourceName, "models", modelName, `v${version}`),
|
|
15941
|
+
{
|
|
15942
|
+
modelName,
|
|
15943
|
+
version,
|
|
15944
|
+
type: "model",
|
|
15945
|
+
modelData: exportedModel,
|
|
15946
|
+
// TensorFlow.js model object (will go to body)
|
|
15947
|
+
metrics: {
|
|
15948
|
+
loss: modelStats.loss,
|
|
15949
|
+
accuracy: modelStats.accuracy,
|
|
15950
|
+
samples: modelStats.samples
|
|
15951
|
+
},
|
|
15952
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15953
|
+
},
|
|
15954
|
+
{ behavior: "body-only" }
|
|
15955
|
+
// Large binary data goes to S3 body
|
|
15956
|
+
);
|
|
15957
|
+
await this._updateVersionInfo(modelName, version);
|
|
15958
|
+
await storage.set(
|
|
15959
|
+
storage.getPluginKey(resourceName, "metadata", modelName, "active"),
|
|
15960
|
+
{
|
|
15961
|
+
modelName,
|
|
15962
|
+
version,
|
|
15963
|
+
type: "reference",
|
|
15964
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15965
|
+
},
|
|
15966
|
+
{ behavior: "body-overflow" }
|
|
15967
|
+
// Small metadata
|
|
15968
|
+
);
|
|
15969
|
+
if (this.config.verbose) {
|
|
15970
|
+
console.log(`[MLPlugin] Saved model "${modelName}" v${version} to S3 (resource=${resourceName}/plugin=ml/models/${modelName}/v${version})`);
|
|
15971
|
+
}
|
|
15972
|
+
} else {
|
|
15973
|
+
await storage.set(
|
|
15974
|
+
storage.getPluginKey(resourceName, "models", modelName, "latest"),
|
|
15975
|
+
{
|
|
15976
|
+
modelName,
|
|
15977
|
+
type: "model",
|
|
15978
|
+
modelData: exportedModel,
|
|
15979
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15980
|
+
},
|
|
15981
|
+
{ behavior: "body-only" }
|
|
15982
|
+
);
|
|
15983
|
+
if (this.config.verbose) {
|
|
15984
|
+
console.log(`[MLPlugin] Saved model "${modelName}" to S3 (resource=${resourceName}/plugin=ml/models/${modelName}/latest)`);
|
|
15985
|
+
}
|
|
13650
15986
|
}
|
|
13651
15987
|
} catch (error) {
|
|
13652
15988
|
console.error(`[MLPlugin] Failed to save model "${modelName}":`, error.message);
|
|
13653
15989
|
}
|
|
13654
15990
|
}
|
|
15991
|
+
/**
|
|
15992
|
+
* Save intermediate training data to plugin storage (incremental - only new samples)
|
|
15993
|
+
* @private
|
|
15994
|
+
*/
|
|
15995
|
+
async _saveTrainingData(modelName, rawData) {
|
|
15996
|
+
try {
|
|
15997
|
+
const storage = this.getStorage();
|
|
15998
|
+
const model = this.models[modelName];
|
|
15999
|
+
const modelConfig = this.config.models[modelName];
|
|
16000
|
+
const resourceName = modelConfig.resource;
|
|
16001
|
+
const modelStats = model.getStats();
|
|
16002
|
+
const enableVersioning = this.config.enableVersioning;
|
|
16003
|
+
const processedData = rawData.map((item) => {
|
|
16004
|
+
const features = {};
|
|
16005
|
+
modelConfig.features.forEach((feature) => {
|
|
16006
|
+
features[feature] = item[feature];
|
|
16007
|
+
});
|
|
16008
|
+
return {
|
|
16009
|
+
id: item.id || `${Date.now()}_${Math.random()}`,
|
|
16010
|
+
// Use record ID or generate
|
|
16011
|
+
features,
|
|
16012
|
+
target: item[modelConfig.target]
|
|
16013
|
+
};
|
|
16014
|
+
});
|
|
16015
|
+
if (enableVersioning) {
|
|
16016
|
+
const version = this._getNextVersion(modelName);
|
|
16017
|
+
const [ok, err, existing] = await tryFn(
|
|
16018
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "history", modelName))
|
|
16019
|
+
);
|
|
16020
|
+
let history = [];
|
|
16021
|
+
let previousSampleIds = /* @__PURE__ */ new Set();
|
|
16022
|
+
if (ok && existing && existing.history) {
|
|
16023
|
+
history = existing.history;
|
|
16024
|
+
history.forEach((entry) => {
|
|
16025
|
+
if (entry.sampleIds) {
|
|
16026
|
+
entry.sampleIds.forEach((id) => previousSampleIds.add(id));
|
|
16027
|
+
}
|
|
16028
|
+
});
|
|
16029
|
+
}
|
|
16030
|
+
const currentSampleIds = new Set(processedData.map((d) => d.id));
|
|
16031
|
+
const newSamples = processedData.filter((d) => !previousSampleIds.has(d.id));
|
|
16032
|
+
const newSampleIds = newSamples.map((d) => d.id);
|
|
16033
|
+
if (newSamples.length > 0) {
|
|
16034
|
+
await storage.set(
|
|
16035
|
+
storage.getPluginKey(resourceName, "training", "data", modelName, `v${version}`),
|
|
16036
|
+
{
|
|
16037
|
+
modelName,
|
|
16038
|
+
version,
|
|
16039
|
+
samples: newSamples,
|
|
16040
|
+
// Only new samples
|
|
16041
|
+
features: modelConfig.features,
|
|
16042
|
+
target: modelConfig.target,
|
|
16043
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16044
|
+
},
|
|
16045
|
+
{ behavior: "body-only" }
|
|
16046
|
+
// Dataset goes to S3 body
|
|
16047
|
+
);
|
|
16048
|
+
}
|
|
16049
|
+
const historyEntry = {
|
|
16050
|
+
version,
|
|
16051
|
+
totalSamples: processedData.length,
|
|
16052
|
+
// Total cumulative
|
|
16053
|
+
newSamples: newSamples.length,
|
|
16054
|
+
// Only new in this version
|
|
16055
|
+
sampleIds: Array.from(currentSampleIds),
|
|
16056
|
+
// All IDs for this version
|
|
16057
|
+
newSampleIds,
|
|
16058
|
+
// IDs of new samples
|
|
16059
|
+
storageKey: newSamples.length > 0 ? `training/data/${modelName}/v${version}` : null,
|
|
16060
|
+
metrics: {
|
|
16061
|
+
loss: modelStats.loss,
|
|
16062
|
+
accuracy: modelStats.accuracy,
|
|
16063
|
+
r2: modelStats.r2
|
|
16064
|
+
},
|
|
16065
|
+
trainedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16066
|
+
};
|
|
16067
|
+
history.push(historyEntry);
|
|
16068
|
+
await storage.set(
|
|
16069
|
+
storage.getPluginKey(resourceName, "training", "history", modelName),
|
|
16070
|
+
{
|
|
16071
|
+
modelName,
|
|
16072
|
+
type: "training_history",
|
|
16073
|
+
totalTrainings: history.length,
|
|
16074
|
+
latestVersion: version,
|
|
16075
|
+
history,
|
|
16076
|
+
// Array of metadata entries (not full data)
|
|
16077
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16078
|
+
},
|
|
16079
|
+
{ behavior: "body-overflow" }
|
|
16080
|
+
// History metadata
|
|
16081
|
+
);
|
|
16082
|
+
if (this.config.verbose) {
|
|
16083
|
+
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})`);
|
|
16084
|
+
}
|
|
16085
|
+
} else {
|
|
16086
|
+
await storage.set(
|
|
16087
|
+
storage.getPluginKey(resourceName, "training", "data", modelName, "latest"),
|
|
16088
|
+
{
|
|
16089
|
+
modelName,
|
|
16090
|
+
type: "training_data",
|
|
16091
|
+
samples: processedData,
|
|
16092
|
+
features: modelConfig.features,
|
|
16093
|
+
target: modelConfig.target,
|
|
16094
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16095
|
+
},
|
|
16096
|
+
{ behavior: "body-only" }
|
|
16097
|
+
);
|
|
16098
|
+
if (this.config.verbose) {
|
|
16099
|
+
console.log(`[MLPlugin] Saved training data for "${modelName}" (${processedData.length} samples) to S3 (resource=${resourceName}/plugin=ml/training/data/${modelName}/latest)`);
|
|
16100
|
+
}
|
|
16101
|
+
}
|
|
16102
|
+
} catch (error) {
|
|
16103
|
+
console.error(`[MLPlugin] Failed to save training data for "${modelName}":`, error.message);
|
|
16104
|
+
}
|
|
16105
|
+
}
|
|
13655
16106
|
/**
|
|
13656
16107
|
* Load model from plugin storage
|
|
13657
16108
|
* @private
|
|
@@ -13659,32 +16110,155 @@ class MLPlugin extends Plugin {
|
|
|
13659
16110
|
async _loadModel(modelName) {
|
|
13660
16111
|
try {
|
|
13661
16112
|
const storage = this.getStorage();
|
|
13662
|
-
const
|
|
13663
|
-
|
|
16113
|
+
const modelConfig = this.config.models[modelName];
|
|
16114
|
+
const resourceName = modelConfig.resource;
|
|
16115
|
+
const enableVersioning = this.config.enableVersioning;
|
|
16116
|
+
if (enableVersioning) {
|
|
16117
|
+
const [okRef, errRef, activeRef] = await tryFn(
|
|
16118
|
+
() => storage.get(storage.getPluginKey(resourceName, "metadata", modelName, "active"))
|
|
16119
|
+
);
|
|
16120
|
+
if (okRef && activeRef && activeRef.version) {
|
|
16121
|
+
const version = activeRef.version;
|
|
16122
|
+
const [ok, err, versionData] = await tryFn(
|
|
16123
|
+
() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version}`))
|
|
16124
|
+
);
|
|
16125
|
+
if (ok && versionData && versionData.modelData) {
|
|
16126
|
+
await this.models[modelName].import(versionData.modelData);
|
|
16127
|
+
if (this.config.verbose) {
|
|
16128
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (active) from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/v${version})`);
|
|
16129
|
+
}
|
|
16130
|
+
return;
|
|
16131
|
+
}
|
|
16132
|
+
}
|
|
16133
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
16134
|
+
if (versionInfo && versionInfo.latestVersion > 0) {
|
|
16135
|
+
const version = versionInfo.latestVersion;
|
|
16136
|
+
const [ok, err, versionData] = await tryFn(
|
|
16137
|
+
() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version}`))
|
|
16138
|
+
);
|
|
16139
|
+
if (ok && versionData && versionData.modelData) {
|
|
16140
|
+
await this.models[modelName].import(versionData.modelData);
|
|
16141
|
+
if (this.config.verbose) {
|
|
16142
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version} (latest) from S3`);
|
|
16143
|
+
}
|
|
16144
|
+
return;
|
|
16145
|
+
}
|
|
16146
|
+
}
|
|
13664
16147
|
if (this.config.verbose) {
|
|
13665
|
-
console.log(`[MLPlugin] No saved model found for "${modelName}"`);
|
|
16148
|
+
console.log(`[MLPlugin] No saved model versions found for "${modelName}"`);
|
|
16149
|
+
}
|
|
16150
|
+
} else {
|
|
16151
|
+
const [ok, err, record] = await tryFn(
|
|
16152
|
+
() => storage.get(storage.getPluginKey(resourceName, "models", modelName, "latest"))
|
|
16153
|
+
);
|
|
16154
|
+
if (!ok || !record || !record.modelData) {
|
|
16155
|
+
if (this.config.verbose) {
|
|
16156
|
+
console.log(`[MLPlugin] No saved model found for "${modelName}"`);
|
|
16157
|
+
}
|
|
16158
|
+
return;
|
|
16159
|
+
}
|
|
16160
|
+
await this.models[modelName].import(record.modelData);
|
|
16161
|
+
if (this.config.verbose) {
|
|
16162
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/latest)`);
|
|
13666
16163
|
}
|
|
13667
|
-
return;
|
|
13668
|
-
}
|
|
13669
|
-
const modelData = JSON.parse(record.data);
|
|
13670
|
-
await this.models[modelName].import(modelData);
|
|
13671
|
-
if (this.config.verbose) {
|
|
13672
|
-
console.log(`[MLPlugin] Loaded model "${modelName}" from plugin storage`);
|
|
13673
16164
|
}
|
|
13674
16165
|
} catch (error) {
|
|
13675
16166
|
console.error(`[MLPlugin] Failed to load model "${modelName}":`, error.message);
|
|
13676
16167
|
}
|
|
13677
16168
|
}
|
|
13678
16169
|
/**
|
|
13679
|
-
*
|
|
16170
|
+
* Load training data from plugin storage (reconstructs specific version from incremental data)
|
|
16171
|
+
* @param {string} modelName - Model name
|
|
16172
|
+
* @param {number} version - Version number (optional, defaults to latest)
|
|
16173
|
+
* @returns {Object|null} Training data or null if not found
|
|
16174
|
+
*/
|
|
16175
|
+
async getTrainingData(modelName, version = null) {
|
|
16176
|
+
try {
|
|
16177
|
+
const storage = this.getStorage();
|
|
16178
|
+
const modelConfig = this.config.models[modelName];
|
|
16179
|
+
const resourceName = modelConfig.resource;
|
|
16180
|
+
const enableVersioning = this.config.enableVersioning;
|
|
16181
|
+
if (!enableVersioning) {
|
|
16182
|
+
const [ok, err, record] = await tryFn(
|
|
16183
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "data", modelName, "latest"))
|
|
16184
|
+
);
|
|
16185
|
+
if (!ok || !record) {
|
|
16186
|
+
if (this.config.verbose) {
|
|
16187
|
+
console.log(`[MLPlugin] No saved training data found for "${modelName}"`);
|
|
16188
|
+
}
|
|
16189
|
+
return null;
|
|
16190
|
+
}
|
|
16191
|
+
return {
|
|
16192
|
+
modelName: record.modelName,
|
|
16193
|
+
samples: record.samples,
|
|
16194
|
+
features: record.features,
|
|
16195
|
+
target: record.target,
|
|
16196
|
+
data: record.samples,
|
|
16197
|
+
savedAt: record.savedAt
|
|
16198
|
+
};
|
|
16199
|
+
}
|
|
16200
|
+
const [okHistory, errHistory, historyData] = await tryFn(
|
|
16201
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "history", modelName))
|
|
16202
|
+
);
|
|
16203
|
+
if (!okHistory || !historyData || !historyData.history) {
|
|
16204
|
+
if (this.config.verbose) {
|
|
16205
|
+
console.log(`[MLPlugin] No training history found for "${modelName}"`);
|
|
16206
|
+
}
|
|
16207
|
+
return null;
|
|
16208
|
+
}
|
|
16209
|
+
const targetVersion = version || historyData.latestVersion;
|
|
16210
|
+
const reconstructedSamples = [];
|
|
16211
|
+
for (const entry of historyData.history) {
|
|
16212
|
+
if (entry.version > targetVersion) break;
|
|
16213
|
+
if (entry.storageKey && entry.newSamples > 0) {
|
|
16214
|
+
const [ok, err, versionData] = await tryFn(
|
|
16215
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "data", modelName, `v${entry.version}`))
|
|
16216
|
+
);
|
|
16217
|
+
if (ok && versionData && versionData.samples) {
|
|
16218
|
+
reconstructedSamples.push(...versionData.samples);
|
|
16219
|
+
}
|
|
16220
|
+
}
|
|
16221
|
+
}
|
|
16222
|
+
const targetEntry = historyData.history.find((e) => e.version === targetVersion);
|
|
16223
|
+
return {
|
|
16224
|
+
modelName,
|
|
16225
|
+
version: targetVersion,
|
|
16226
|
+
samples: reconstructedSamples,
|
|
16227
|
+
totalSamples: reconstructedSamples.length,
|
|
16228
|
+
features: modelConfig.features,
|
|
16229
|
+
target: modelConfig.target,
|
|
16230
|
+
metrics: targetEntry?.metrics,
|
|
16231
|
+
savedAt: targetEntry?.trainedAt
|
|
16232
|
+
};
|
|
16233
|
+
} catch (error) {
|
|
16234
|
+
console.error(`[MLPlugin] Failed to load training data for "${modelName}":`, error.message);
|
|
16235
|
+
return null;
|
|
16236
|
+
}
|
|
16237
|
+
}
|
|
16238
|
+
/**
|
|
16239
|
+
* Delete model from plugin storage (all versions)
|
|
13680
16240
|
* @private
|
|
13681
16241
|
*/
|
|
13682
16242
|
async _deleteModel(modelName) {
|
|
13683
16243
|
try {
|
|
13684
16244
|
const storage = this.getStorage();
|
|
13685
|
-
|
|
16245
|
+
const modelConfig = this.config.models[modelName];
|
|
16246
|
+
const resourceName = modelConfig.resource;
|
|
16247
|
+
const enableVersioning = this.config.enableVersioning;
|
|
16248
|
+
if (enableVersioning) {
|
|
16249
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
16250
|
+
if (versionInfo && versionInfo.latestVersion > 0) {
|
|
16251
|
+
for (let v = 1; v <= versionInfo.latestVersion; v++) {
|
|
16252
|
+
await storage.delete(storage.getPluginKey(resourceName, "models", modelName, `v${v}`));
|
|
16253
|
+
}
|
|
16254
|
+
}
|
|
16255
|
+
await storage.delete(storage.getPluginKey(resourceName, "metadata", modelName, "active"));
|
|
16256
|
+
await storage.delete(storage.getPluginKey(resourceName, "metadata", modelName, "versions"));
|
|
16257
|
+
} else {
|
|
16258
|
+
await storage.delete(storage.getPluginKey(resourceName, "models", modelName, "latest"));
|
|
16259
|
+
}
|
|
13686
16260
|
if (this.config.verbose) {
|
|
13687
|
-
console.log(`[MLPlugin] Deleted model "${modelName}" from plugin
|
|
16261
|
+
console.log(`[MLPlugin] Deleted model "${modelName}" from S3 (resource=${resourceName}/plugin=ml/models/${modelName}/)`);
|
|
13688
16262
|
}
|
|
13689
16263
|
} catch (error) {
|
|
13690
16264
|
if (this.config.verbose) {
|
|
@@ -13692,6 +16266,247 @@ class MLPlugin extends Plugin {
|
|
|
13692
16266
|
}
|
|
13693
16267
|
}
|
|
13694
16268
|
}
|
|
16269
|
+
/**
|
|
16270
|
+
* Delete training data from plugin storage (all versions)
|
|
16271
|
+
* @private
|
|
16272
|
+
*/
|
|
16273
|
+
async _deleteTrainingData(modelName) {
|
|
16274
|
+
try {
|
|
16275
|
+
const storage = this.getStorage();
|
|
16276
|
+
const modelConfig = this.config.models[modelName];
|
|
16277
|
+
const resourceName = modelConfig.resource;
|
|
16278
|
+
const enableVersioning = this.config.enableVersioning;
|
|
16279
|
+
if (enableVersioning) {
|
|
16280
|
+
const [ok, err, historyData] = await tryFn(
|
|
16281
|
+
() => storage.get(storage.getPluginKey(resourceName, "training", "history", modelName))
|
|
16282
|
+
);
|
|
16283
|
+
if (ok && historyData && historyData.history) {
|
|
16284
|
+
for (const entry of historyData.history) {
|
|
16285
|
+
if (entry.storageKey) {
|
|
16286
|
+
await storage.delete(storage.getPluginKey(resourceName, "training", "data", modelName, `v${entry.version}`));
|
|
16287
|
+
}
|
|
16288
|
+
}
|
|
16289
|
+
}
|
|
16290
|
+
await storage.delete(storage.getPluginKey(resourceName, "training", "history", modelName));
|
|
16291
|
+
} else {
|
|
16292
|
+
await storage.delete(storage.getPluginKey(resourceName, "training", "data", modelName, "latest"));
|
|
16293
|
+
}
|
|
16294
|
+
if (this.config.verbose) {
|
|
16295
|
+
console.log(`[MLPlugin] Deleted training data for "${modelName}" from S3 (resource=${resourceName}/plugin=ml/training/)`);
|
|
16296
|
+
}
|
|
16297
|
+
} catch (error) {
|
|
16298
|
+
if (this.config.verbose) {
|
|
16299
|
+
console.log(`[MLPlugin] Could not delete training data "${modelName}": ${error.message}`);
|
|
16300
|
+
}
|
|
16301
|
+
}
|
|
16302
|
+
}
|
|
16303
|
+
/**
|
|
16304
|
+
* List all versions of a model
|
|
16305
|
+
* @param {string} modelName - Model name
|
|
16306
|
+
* @returns {Array} List of version info
|
|
16307
|
+
*/
|
|
16308
|
+
async listModelVersions(modelName) {
|
|
16309
|
+
if (!this.config.enableVersioning) {
|
|
16310
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16311
|
+
}
|
|
16312
|
+
try {
|
|
16313
|
+
const storage = this.getStorage();
|
|
16314
|
+
const modelConfig = this.config.models[modelName];
|
|
16315
|
+
const resourceName = modelConfig.resource;
|
|
16316
|
+
const versionInfo = this.modelVersions.get(modelName) || { latestVersion: 0 };
|
|
16317
|
+
const versions = [];
|
|
16318
|
+
for (let v = 1; v <= versionInfo.latestVersion; v++) {
|
|
16319
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${v}`)));
|
|
16320
|
+
if (ok && versionData) {
|
|
16321
|
+
versions.push({
|
|
16322
|
+
version: v,
|
|
16323
|
+
savedAt: versionData.savedAt,
|
|
16324
|
+
isCurrent: v === versionInfo.currentVersion,
|
|
16325
|
+
metrics: versionData.metrics
|
|
16326
|
+
});
|
|
16327
|
+
}
|
|
16328
|
+
}
|
|
16329
|
+
return versions;
|
|
16330
|
+
} catch (error) {
|
|
16331
|
+
console.error(`[MLPlugin] Failed to list versions for "${modelName}":`, error.message);
|
|
16332
|
+
return [];
|
|
16333
|
+
}
|
|
16334
|
+
}
|
|
16335
|
+
/**
|
|
16336
|
+
* Load a specific version of a model
|
|
16337
|
+
* @param {string} modelName - Model name
|
|
16338
|
+
* @param {number} version - Version number
|
|
16339
|
+
*/
|
|
16340
|
+
async loadModelVersion(modelName, version) {
|
|
16341
|
+
if (!this.config.enableVersioning) {
|
|
16342
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16343
|
+
}
|
|
16344
|
+
if (!this.models[modelName]) {
|
|
16345
|
+
throw new ModelNotFoundError(`Model "${modelName}" not found`, { modelName });
|
|
16346
|
+
}
|
|
16347
|
+
try {
|
|
16348
|
+
const storage = this.getStorage();
|
|
16349
|
+
const modelConfig = this.config.models[modelName];
|
|
16350
|
+
const resourceName = modelConfig.resource;
|
|
16351
|
+
const [ok, err, versionData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version}`)));
|
|
16352
|
+
if (!ok || !versionData) {
|
|
16353
|
+
throw new MLError(`Version ${version} not found for model "${modelName}"`, { modelName, version });
|
|
16354
|
+
}
|
|
16355
|
+
if (!versionData.modelData) {
|
|
16356
|
+
throw new MLError(`Model data not found in version ${version}`, { modelName, version });
|
|
16357
|
+
}
|
|
16358
|
+
await this.models[modelName].import(versionData.modelData);
|
|
16359
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
16360
|
+
if (versionInfo) {
|
|
16361
|
+
versionInfo.currentVersion = version;
|
|
16362
|
+
this.modelVersions.set(modelName, versionInfo);
|
|
16363
|
+
}
|
|
16364
|
+
if (this.config.verbose) {
|
|
16365
|
+
console.log(`[MLPlugin] Loaded model "${modelName}" v${version}`);
|
|
16366
|
+
}
|
|
16367
|
+
return {
|
|
16368
|
+
version,
|
|
16369
|
+
metrics: versionData.metrics ? JSON.parse(versionData.metrics) : {},
|
|
16370
|
+
savedAt: versionData.savedAt
|
|
16371
|
+
};
|
|
16372
|
+
} catch (error) {
|
|
16373
|
+
console.error(`[MLPlugin] Failed to load version ${version} for "${modelName}":`, error.message);
|
|
16374
|
+
throw error;
|
|
16375
|
+
}
|
|
16376
|
+
}
|
|
16377
|
+
/**
|
|
16378
|
+
* Set active version for a model (used for predictions)
|
|
16379
|
+
* @param {string} modelName - Model name
|
|
16380
|
+
* @param {number} version - Version number
|
|
16381
|
+
*/
|
|
16382
|
+
async setActiveVersion(modelName, version) {
|
|
16383
|
+
if (!this.config.enableVersioning) {
|
|
16384
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16385
|
+
}
|
|
16386
|
+
const modelConfig = this.config.models[modelName];
|
|
16387
|
+
const resourceName = modelConfig.resource;
|
|
16388
|
+
await this.loadModelVersion(modelName, version);
|
|
16389
|
+
await this._updateVersionInfo(modelName, version);
|
|
16390
|
+
const storage = this.getStorage();
|
|
16391
|
+
await storage.set(storage.getPluginKey(resourceName, "metadata", modelName, "active"), {
|
|
16392
|
+
modelName,
|
|
16393
|
+
version,
|
|
16394
|
+
type: "reference",
|
|
16395
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16396
|
+
});
|
|
16397
|
+
if (this.config.verbose) {
|
|
16398
|
+
console.log(`[MLPlugin] Set model "${modelName}" active version to v${version}`);
|
|
16399
|
+
}
|
|
16400
|
+
return { modelName, version };
|
|
16401
|
+
}
|
|
16402
|
+
/**
|
|
16403
|
+
* Get training history for a model
|
|
16404
|
+
* @param {string} modelName - Model name
|
|
16405
|
+
* @returns {Array} Training history
|
|
16406
|
+
*/
|
|
16407
|
+
async getTrainingHistory(modelName) {
|
|
16408
|
+
if (!this.config.enableVersioning) {
|
|
16409
|
+
return await this.getTrainingData(modelName);
|
|
16410
|
+
}
|
|
16411
|
+
try {
|
|
16412
|
+
const storage = this.getStorage();
|
|
16413
|
+
const modelConfig = this.config.models[modelName];
|
|
16414
|
+
const resourceName = modelConfig.resource;
|
|
16415
|
+
const [ok, err, historyData] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "training", "history", modelName)));
|
|
16416
|
+
if (!ok || !historyData) {
|
|
16417
|
+
return null;
|
|
16418
|
+
}
|
|
16419
|
+
return {
|
|
16420
|
+
modelName: historyData.modelName,
|
|
16421
|
+
totalTrainings: historyData.totalTrainings,
|
|
16422
|
+
latestVersion: historyData.latestVersion,
|
|
16423
|
+
history: JSON.parse(historyData.history),
|
|
16424
|
+
updatedAt: historyData.updatedAt
|
|
16425
|
+
};
|
|
16426
|
+
} catch (error) {
|
|
16427
|
+
console.error(`[MLPlugin] Failed to load training history for "${modelName}":`, error.message);
|
|
16428
|
+
return null;
|
|
16429
|
+
}
|
|
16430
|
+
}
|
|
16431
|
+
/**
|
|
16432
|
+
* Compare metrics between two versions
|
|
16433
|
+
* @param {string} modelName - Model name
|
|
16434
|
+
* @param {number} version1 - First version
|
|
16435
|
+
* @param {number} version2 - Second version
|
|
16436
|
+
* @returns {Object} Comparison results
|
|
16437
|
+
*/
|
|
16438
|
+
async compareVersions(modelName, version1, version2) {
|
|
16439
|
+
if (!this.config.enableVersioning) {
|
|
16440
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16441
|
+
}
|
|
16442
|
+
try {
|
|
16443
|
+
const storage = this.getStorage();
|
|
16444
|
+
const modelConfig = this.config.models[modelName];
|
|
16445
|
+
const resourceName = modelConfig.resource;
|
|
16446
|
+
const [ok1, err1, v1Data] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version1}`)));
|
|
16447
|
+
const [ok2, err2, v2Data] = await tryFn(() => storage.get(storage.getPluginKey(resourceName, "models", modelName, `v${version2}`)));
|
|
16448
|
+
if (!ok1 || !v1Data) {
|
|
16449
|
+
throw new MLError(`Version ${version1} not found`, { modelName, version: version1 });
|
|
16450
|
+
}
|
|
16451
|
+
if (!ok2 || !v2Data) {
|
|
16452
|
+
throw new MLError(`Version ${version2} not found`, { modelName, version: version2 });
|
|
16453
|
+
}
|
|
16454
|
+
const metrics1 = v1Data.metrics ? JSON.parse(v1Data.metrics) : {};
|
|
16455
|
+
const metrics2 = v2Data.metrics ? JSON.parse(v2Data.metrics) : {};
|
|
16456
|
+
return {
|
|
16457
|
+
modelName,
|
|
16458
|
+
version1: {
|
|
16459
|
+
version: version1,
|
|
16460
|
+
savedAt: v1Data.savedAt,
|
|
16461
|
+
metrics: metrics1
|
|
16462
|
+
},
|
|
16463
|
+
version2: {
|
|
16464
|
+
version: version2,
|
|
16465
|
+
savedAt: v2Data.savedAt,
|
|
16466
|
+
metrics: metrics2
|
|
16467
|
+
},
|
|
16468
|
+
improvement: {
|
|
16469
|
+
loss: metrics1.loss && metrics2.loss ? ((metrics1.loss - metrics2.loss) / metrics1.loss * 100).toFixed(2) + "%" : "N/A",
|
|
16470
|
+
accuracy: metrics1.accuracy && metrics2.accuracy ? ((metrics2.accuracy - metrics1.accuracy) / metrics1.accuracy * 100).toFixed(2) + "%" : "N/A"
|
|
16471
|
+
}
|
|
16472
|
+
};
|
|
16473
|
+
} catch (error) {
|
|
16474
|
+
console.error(`[MLPlugin] Failed to compare versions for "${modelName}":`, error.message);
|
|
16475
|
+
throw error;
|
|
16476
|
+
}
|
|
16477
|
+
}
|
|
16478
|
+
/**
|
|
16479
|
+
* Rollback to a previous version
|
|
16480
|
+
* @param {string} modelName - Model name
|
|
16481
|
+
* @param {number} version - Version to rollback to (defaults to previous version)
|
|
16482
|
+
* @returns {Object} Rollback info
|
|
16483
|
+
*/
|
|
16484
|
+
async rollbackVersion(modelName, version = null) {
|
|
16485
|
+
if (!this.config.enableVersioning) {
|
|
16486
|
+
throw new MLError("Versioning is not enabled", { modelName });
|
|
16487
|
+
}
|
|
16488
|
+
const versionInfo = this.modelVersions.get(modelName);
|
|
16489
|
+
if (!versionInfo) {
|
|
16490
|
+
throw new MLError(`No version info found for model "${modelName}"`, { modelName });
|
|
16491
|
+
}
|
|
16492
|
+
const targetVersion = version !== null ? version : Math.max(1, versionInfo.currentVersion - 1);
|
|
16493
|
+
if (targetVersion === versionInfo.currentVersion) {
|
|
16494
|
+
throw new MLError("Cannot rollback to the same version", { modelName, version: targetVersion });
|
|
16495
|
+
}
|
|
16496
|
+
if (targetVersion < 1 || targetVersion > versionInfo.latestVersion) {
|
|
16497
|
+
throw new MLError(`Invalid version ${targetVersion}`, { modelName, version: targetVersion, latestVersion: versionInfo.latestVersion });
|
|
16498
|
+
}
|
|
16499
|
+
const result = await this.setActiveVersion(modelName, targetVersion);
|
|
16500
|
+
if (this.config.verbose) {
|
|
16501
|
+
console.log(`[MLPlugin] Rolled back model "${modelName}" from v${versionInfo.currentVersion} to v${targetVersion}`);
|
|
16502
|
+
}
|
|
16503
|
+
return {
|
|
16504
|
+
modelName,
|
|
16505
|
+
previousVersion: versionInfo.currentVersion,
|
|
16506
|
+
currentVersion: targetVersion,
|
|
16507
|
+
...result
|
|
16508
|
+
};
|
|
16509
|
+
}
|
|
13695
16510
|
}
|
|
13696
16511
|
|
|
13697
16512
|
class SqsConsumer {
|
|
@@ -14085,7 +16900,7 @@ class RelationPlugin extends Plugin {
|
|
|
14085
16900
|
if (this.verbose) {
|
|
14086
16901
|
console.log(`[RelationPlugin] Installed with ${Object.keys(this.relations).length} resources`);
|
|
14087
16902
|
}
|
|
14088
|
-
this.emit("installed", {
|
|
16903
|
+
this.emit("db:plugin:installed", {
|
|
14089
16904
|
plugin: "RelationPlugin",
|
|
14090
16905
|
resources: Object.keys(this.relations)
|
|
14091
16906
|
});
|
|
@@ -17660,7 +20475,7 @@ class S3Client extends EventEmitter {
|
|
|
17660
20475
|
return client;
|
|
17661
20476
|
}
|
|
17662
20477
|
async sendCommand(command) {
|
|
17663
|
-
this.emit("
|
|
20478
|
+
this.emit("cl:request", command.constructor.name, command.input);
|
|
17664
20479
|
const [ok, err, response] = await tryFn(() => this.client.send(command));
|
|
17665
20480
|
if (!ok) {
|
|
17666
20481
|
const bucket = this.config.bucket;
|
|
@@ -17672,7 +20487,7 @@ class S3Client extends EventEmitter {
|
|
|
17672
20487
|
commandInput: command.input
|
|
17673
20488
|
});
|
|
17674
20489
|
}
|
|
17675
|
-
this.emit("
|
|
20490
|
+
this.emit("cl:response", command.constructor.name, response, command.input);
|
|
17676
20491
|
return response;
|
|
17677
20492
|
}
|
|
17678
20493
|
async putObject({ key, metadata, contentType, body, contentEncoding, contentLength, ifMatch }) {
|
|
@@ -17697,7 +20512,7 @@ class S3Client extends EventEmitter {
|
|
|
17697
20512
|
if (contentLength !== void 0) options.ContentLength = contentLength;
|
|
17698
20513
|
if (ifMatch !== void 0) options.IfMatch = ifMatch;
|
|
17699
20514
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new clientS3.PutObjectCommand(options)));
|
|
17700
|
-
this.emit("
|
|
20515
|
+
this.emit("cl:PutObject", err || response, { key, metadata, contentType, body, contentEncoding, contentLength });
|
|
17701
20516
|
if (!ok) {
|
|
17702
20517
|
throw mapAwsError(err, {
|
|
17703
20518
|
bucket: this.config.bucket,
|
|
@@ -17725,7 +20540,7 @@ class S3Client extends EventEmitter {
|
|
|
17725
20540
|
}
|
|
17726
20541
|
return res;
|
|
17727
20542
|
});
|
|
17728
|
-
this.emit("
|
|
20543
|
+
this.emit("cl:GetObject", err || response, { key });
|
|
17729
20544
|
if (!ok) {
|
|
17730
20545
|
throw mapAwsError(err, {
|
|
17731
20546
|
bucket: this.config.bucket,
|
|
@@ -17753,7 +20568,7 @@ class S3Client extends EventEmitter {
|
|
|
17753
20568
|
}
|
|
17754
20569
|
return res;
|
|
17755
20570
|
});
|
|
17756
|
-
this.emit("
|
|
20571
|
+
this.emit("cl:HeadObject", err || response, { key });
|
|
17757
20572
|
if (!ok) {
|
|
17758
20573
|
throw mapAwsError(err, {
|
|
17759
20574
|
bucket: this.config.bucket,
|
|
@@ -17786,7 +20601,7 @@ class S3Client extends EventEmitter {
|
|
|
17786
20601
|
options.ContentType = contentType;
|
|
17787
20602
|
}
|
|
17788
20603
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new clientS3.CopyObjectCommand(options)));
|
|
17789
|
-
this.emit("
|
|
20604
|
+
this.emit("cl:CopyObject", err || response, { from, to, metadataDirective });
|
|
17790
20605
|
if (!ok) {
|
|
17791
20606
|
throw mapAwsError(err, {
|
|
17792
20607
|
bucket: this.config.bucket,
|
|
@@ -17811,7 +20626,7 @@ class S3Client extends EventEmitter {
|
|
|
17811
20626
|
Key: keyPrefix ? path$1.join(keyPrefix, key) : key
|
|
17812
20627
|
};
|
|
17813
20628
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new clientS3.DeleteObjectCommand(options)));
|
|
17814
|
-
this.emit("
|
|
20629
|
+
this.emit("cl:DeleteObject", err || response, { key });
|
|
17815
20630
|
if (!ok) {
|
|
17816
20631
|
throw mapAwsError(err, {
|
|
17817
20632
|
bucket: this.config.bucket,
|
|
@@ -17851,7 +20666,7 @@ class S3Client extends EventEmitter {
|
|
|
17851
20666
|
deleted: results,
|
|
17852
20667
|
notFound: errors
|
|
17853
20668
|
};
|
|
17854
|
-
this.emit("
|
|
20669
|
+
this.emit("cl:DeleteObjects", report, keys);
|
|
17855
20670
|
return report;
|
|
17856
20671
|
}
|
|
17857
20672
|
/**
|
|
@@ -17881,7 +20696,7 @@ class S3Client extends EventEmitter {
|
|
|
17881
20696
|
const deleteResponse = await this.client.send(deleteCommand);
|
|
17882
20697
|
const deletedCount = deleteResponse.Deleted ? deleteResponse.Deleted.length : 0;
|
|
17883
20698
|
totalDeleted += deletedCount;
|
|
17884
|
-
this.emit("
|
|
20699
|
+
this.emit("cl:DeleteAll", {
|
|
17885
20700
|
prefix,
|
|
17886
20701
|
batch: deletedCount,
|
|
17887
20702
|
total: totalDeleted
|
|
@@ -17889,7 +20704,7 @@ class S3Client extends EventEmitter {
|
|
|
17889
20704
|
}
|
|
17890
20705
|
continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : void 0;
|
|
17891
20706
|
} while (continuationToken);
|
|
17892
|
-
this.emit("
|
|
20707
|
+
this.emit("cl:DeleteAllComplete", {
|
|
17893
20708
|
prefix,
|
|
17894
20709
|
totalDeleted
|
|
17895
20710
|
});
|
|
@@ -17920,7 +20735,7 @@ class S3Client extends EventEmitter {
|
|
|
17920
20735
|
if (!ok) {
|
|
17921
20736
|
throw new UnknownError("Unknown error in listObjects", { prefix, bucket: this.config.bucket, original: err });
|
|
17922
20737
|
}
|
|
17923
|
-
this.emit("
|
|
20738
|
+
this.emit("cl:ListObjects", response, options);
|
|
17924
20739
|
return response;
|
|
17925
20740
|
}
|
|
17926
20741
|
async count({ prefix } = {}) {
|
|
@@ -17937,7 +20752,7 @@ class S3Client extends EventEmitter {
|
|
|
17937
20752
|
truncated = response.IsTruncated || false;
|
|
17938
20753
|
continuationToken = response.NextContinuationToken;
|
|
17939
20754
|
}
|
|
17940
|
-
this.emit("
|
|
20755
|
+
this.emit("cl:Count", count, { prefix });
|
|
17941
20756
|
return count;
|
|
17942
20757
|
}
|
|
17943
20758
|
async getAllKeys({ prefix } = {}) {
|
|
@@ -17959,7 +20774,7 @@ class S3Client extends EventEmitter {
|
|
|
17959
20774
|
if (this.config.keyPrefix) {
|
|
17960
20775
|
keys = keys.map((x) => x.replace(this.config.keyPrefix, "")).map((x) => x.startsWith("/") ? x.replace(`/`, "") : x);
|
|
17961
20776
|
}
|
|
17962
|
-
this.emit("
|
|
20777
|
+
this.emit("cl:GetAllKeys", keys, { prefix });
|
|
17963
20778
|
return keys;
|
|
17964
20779
|
}
|
|
17965
20780
|
async getContinuationTokenAfterOffset(params = {}) {
|
|
@@ -17988,7 +20803,7 @@ class S3Client extends EventEmitter {
|
|
|
17988
20803
|
break;
|
|
17989
20804
|
}
|
|
17990
20805
|
}
|
|
17991
|
-
this.emit("
|
|
20806
|
+
this.emit("cl:GetContinuationTokenAfterOffset", continuationToken || null, params);
|
|
17992
20807
|
return continuationToken || null;
|
|
17993
20808
|
}
|
|
17994
20809
|
async getKeysPage(params = {}) {
|
|
@@ -18006,7 +20821,7 @@ class S3Client extends EventEmitter {
|
|
|
18006
20821
|
offset
|
|
18007
20822
|
});
|
|
18008
20823
|
if (!continuationToken) {
|
|
18009
|
-
this.emit("
|
|
20824
|
+
this.emit("cl:GetKeysPage", [], params);
|
|
18010
20825
|
return [];
|
|
18011
20826
|
}
|
|
18012
20827
|
}
|
|
@@ -18029,7 +20844,7 @@ class S3Client extends EventEmitter {
|
|
|
18029
20844
|
if (this.config.keyPrefix) {
|
|
18030
20845
|
keys = keys.map((x) => x.replace(this.config.keyPrefix, "")).map((x) => x.startsWith("/") ? x.replace(`/`, "") : x);
|
|
18031
20846
|
}
|
|
18032
|
-
this.emit("
|
|
20847
|
+
this.emit("cl:GetKeysPage", keys, params);
|
|
18033
20848
|
return keys;
|
|
18034
20849
|
}
|
|
18035
20850
|
async moveAllObjects({ prefixFrom, prefixTo }) {
|
|
@@ -18047,7 +20862,7 @@ class S3Client extends EventEmitter {
|
|
|
18047
20862
|
}
|
|
18048
20863
|
return to;
|
|
18049
20864
|
});
|
|
18050
|
-
this.emit("
|
|
20865
|
+
this.emit("cl:MoveAllObjects", { results, errors }, { prefixFrom, prefixTo });
|
|
18051
20866
|
if (errors.length > 0) {
|
|
18052
20867
|
throw new UnknownError("Some objects could not be moved", {
|
|
18053
20868
|
bucket: this.config.bucket,
|
|
@@ -20902,6 +23717,20 @@ ${errorDetails}`,
|
|
|
20902
23717
|
if (typeof body === "object") return Buffer.byteLength(JSON.stringify(body), "utf8");
|
|
20903
23718
|
return Buffer.byteLength(String(body), "utf8");
|
|
20904
23719
|
}
|
|
23720
|
+
/**
|
|
23721
|
+
* Emit standardized events with optional ID-specific variant
|
|
23722
|
+
*
|
|
23723
|
+
* @private
|
|
23724
|
+
* @param {string} event - Event name
|
|
23725
|
+
* @param {Object} payload - Event payload
|
|
23726
|
+
* @param {string} [id] - Optional ID for ID-specific events
|
|
23727
|
+
*/
|
|
23728
|
+
_emitStandardized(event, payload, id = null) {
|
|
23729
|
+
this.emit(event, payload);
|
|
23730
|
+
if (id) {
|
|
23731
|
+
this.emit(`${event}:${id}`, payload);
|
|
23732
|
+
}
|
|
23733
|
+
}
|
|
20905
23734
|
/**
|
|
20906
23735
|
* Insert a new resource object
|
|
20907
23736
|
* @param {Object} attributes - Resource attributes
|
|
@@ -21042,11 +23871,11 @@ ${errorDetails}`,
|
|
|
21042
23871
|
for (const hook of nonPartitionHooks) {
|
|
21043
23872
|
finalResult = await hook(finalResult);
|
|
21044
23873
|
}
|
|
21045
|
-
this.
|
|
23874
|
+
this._emitStandardized("inserted", finalResult, finalResult?.id || insertedObject?.id);
|
|
21046
23875
|
return finalResult;
|
|
21047
23876
|
} else {
|
|
21048
23877
|
const finalResult = await this.executeHooks("afterInsert", insertedObject);
|
|
21049
|
-
this.
|
|
23878
|
+
this._emitStandardized("inserted", finalResult, finalResult?.id || insertedObject?.id);
|
|
21050
23879
|
return finalResult;
|
|
21051
23880
|
}
|
|
21052
23881
|
}
|
|
@@ -21110,7 +23939,7 @@ ${errorDetails}`,
|
|
|
21110
23939
|
data = await this.applyVersionMapping(data, objectVersion, this.version);
|
|
21111
23940
|
}
|
|
21112
23941
|
data = await this.executeHooks("afterGet", data);
|
|
21113
|
-
this.
|
|
23942
|
+
this._emitWithDeprecation("get", "fetched", data, data.id);
|
|
21114
23943
|
const value = data;
|
|
21115
23944
|
return value;
|
|
21116
23945
|
}
|
|
@@ -21361,19 +24190,19 @@ ${errorDetails}`,
|
|
|
21361
24190
|
for (const hook of nonPartitionHooks) {
|
|
21362
24191
|
finalResult = await hook(finalResult);
|
|
21363
24192
|
}
|
|
21364
|
-
this.
|
|
24193
|
+
this._emitStandardized("updated", {
|
|
21365
24194
|
...updatedData,
|
|
21366
24195
|
$before: { ...originalData },
|
|
21367
24196
|
$after: { ...finalResult }
|
|
21368
|
-
});
|
|
24197
|
+
}, updatedData.id);
|
|
21369
24198
|
return finalResult;
|
|
21370
24199
|
} else {
|
|
21371
24200
|
const finalResult = await this.executeHooks("afterUpdate", updatedData);
|
|
21372
|
-
this.
|
|
24201
|
+
this._emitStandardized("updated", {
|
|
21373
24202
|
...updatedData,
|
|
21374
24203
|
$before: { ...originalData },
|
|
21375
24204
|
$after: { ...finalResult }
|
|
21376
|
-
});
|
|
24205
|
+
}, updatedData.id);
|
|
21377
24206
|
return finalResult;
|
|
21378
24207
|
}
|
|
21379
24208
|
}
|
|
@@ -21772,11 +24601,11 @@ ${errorDetails}`,
|
|
|
21772
24601
|
for (const hook of nonPartitionHooks) {
|
|
21773
24602
|
finalResult = await hook(finalResult);
|
|
21774
24603
|
}
|
|
21775
|
-
this.
|
|
24604
|
+
this._emitStandardized("updated", {
|
|
21776
24605
|
...updatedData,
|
|
21777
24606
|
$before: { ...originalData },
|
|
21778
24607
|
$after: { ...finalResult }
|
|
21779
|
-
});
|
|
24608
|
+
}, updatedData.id);
|
|
21780
24609
|
return {
|
|
21781
24610
|
success: true,
|
|
21782
24611
|
data: finalResult,
|
|
@@ -21785,11 +24614,11 @@ ${errorDetails}`,
|
|
|
21785
24614
|
} else {
|
|
21786
24615
|
await this.handlePartitionReferenceUpdates(oldData, newData);
|
|
21787
24616
|
const finalResult = await this.executeHooks("afterUpdate", updatedData);
|
|
21788
|
-
this.
|
|
24617
|
+
this._emitStandardized("updated", {
|
|
21789
24618
|
...updatedData,
|
|
21790
24619
|
$before: { ...originalData },
|
|
21791
24620
|
$after: { ...finalResult }
|
|
21792
|
-
});
|
|
24621
|
+
}, updatedData.id);
|
|
21793
24622
|
return {
|
|
21794
24623
|
success: true,
|
|
21795
24624
|
data: finalResult,
|
|
@@ -21820,11 +24649,11 @@ ${errorDetails}`,
|
|
|
21820
24649
|
await this.executeHooks("beforeDelete", objectData);
|
|
21821
24650
|
const key = this.getResourceKey(id);
|
|
21822
24651
|
const [ok2, err2, response] = await tryFn(() => this.client.deleteObject(key));
|
|
21823
|
-
this.
|
|
24652
|
+
this._emitWithDeprecation("delete", "deleted", {
|
|
21824
24653
|
...objectData,
|
|
21825
24654
|
$before: { ...objectData },
|
|
21826
24655
|
$after: null
|
|
21827
|
-
});
|
|
24656
|
+
}, id);
|
|
21828
24657
|
if (deleteError) {
|
|
21829
24658
|
throw mapAwsError(deleteError, {
|
|
21830
24659
|
bucket: this.client.config.bucket,
|
|
@@ -21948,7 +24777,7 @@ ${errorDetails}`,
|
|
|
21948
24777
|
}
|
|
21949
24778
|
const count = await this.client.count({ prefix });
|
|
21950
24779
|
await this.executeHooks("afterCount", { count, partition, partitionValues });
|
|
21951
|
-
this.
|
|
24780
|
+
this._emitWithDeprecation("count", "counted", count);
|
|
21952
24781
|
return count;
|
|
21953
24782
|
}
|
|
21954
24783
|
/**
|
|
@@ -21971,7 +24800,7 @@ ${errorDetails}`,
|
|
|
21971
24800
|
const result = await this.insert(attributes);
|
|
21972
24801
|
return result;
|
|
21973
24802
|
});
|
|
21974
|
-
this.
|
|
24803
|
+
this._emitWithDeprecation("insertMany", "inserted-many", objects.length);
|
|
21975
24804
|
return results;
|
|
21976
24805
|
}
|
|
21977
24806
|
/**
|
|
@@ -22006,7 +24835,7 @@ ${errorDetails}`,
|
|
|
22006
24835
|
return response;
|
|
22007
24836
|
});
|
|
22008
24837
|
await this.executeHooks("afterDeleteMany", { ids, results });
|
|
22009
|
-
this.
|
|
24838
|
+
this._emitWithDeprecation("deleteMany", "deleted-many", ids.length);
|
|
22010
24839
|
return results;
|
|
22011
24840
|
}
|
|
22012
24841
|
async deleteAll() {
|
|
@@ -22015,7 +24844,7 @@ ${errorDetails}`,
|
|
|
22015
24844
|
}
|
|
22016
24845
|
const prefix = `resource=${this.name}/data`;
|
|
22017
24846
|
const deletedCount = await this.client.deleteAll({ prefix });
|
|
22018
|
-
this.
|
|
24847
|
+
this._emitWithDeprecation("deleteAll", "deleted-all", {
|
|
22019
24848
|
version: this.version,
|
|
22020
24849
|
prefix,
|
|
22021
24850
|
deletedCount
|
|
@@ -22032,7 +24861,7 @@ ${errorDetails}`,
|
|
|
22032
24861
|
}
|
|
22033
24862
|
const prefix = `resource=${this.name}`;
|
|
22034
24863
|
const deletedCount = await this.client.deleteAll({ prefix });
|
|
22035
|
-
this.
|
|
24864
|
+
this._emitWithDeprecation("deleteAllData", "deleted-all-data", {
|
|
22036
24865
|
resource: this.name,
|
|
22037
24866
|
prefix,
|
|
22038
24867
|
deletedCount
|
|
@@ -22102,7 +24931,7 @@ ${errorDetails}`,
|
|
|
22102
24931
|
const idPart = parts.find((part) => part.startsWith("id="));
|
|
22103
24932
|
return idPart ? idPart.replace("id=", "") : null;
|
|
22104
24933
|
}).filter(Boolean);
|
|
22105
|
-
this.
|
|
24934
|
+
this._emitWithDeprecation("listIds", "listed-ids", ids.length);
|
|
22106
24935
|
return ids;
|
|
22107
24936
|
}
|
|
22108
24937
|
/**
|
|
@@ -22144,12 +24973,12 @@ ${errorDetails}`,
|
|
|
22144
24973
|
const [ok, err, ids] = await tryFn(() => this.listIds({ limit, offset }));
|
|
22145
24974
|
if (!ok) throw err;
|
|
22146
24975
|
const results = await this.processListResults(ids, "main");
|
|
22147
|
-
this.
|
|
24976
|
+
this._emitWithDeprecation("list", "listed", { count: results.length, errors: 0 });
|
|
22148
24977
|
return results;
|
|
22149
24978
|
}
|
|
22150
24979
|
async listPartition({ partition, partitionValues, limit, offset = 0 }) {
|
|
22151
24980
|
if (!this.config.partitions?.[partition]) {
|
|
22152
|
-
this.
|
|
24981
|
+
this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 0 });
|
|
22153
24982
|
return [];
|
|
22154
24983
|
}
|
|
22155
24984
|
const partitionDef = this.config.partitions[partition];
|
|
@@ -22159,7 +24988,7 @@ ${errorDetails}`,
|
|
|
22159
24988
|
const ids = this.extractIdsFromKeys(keys).slice(offset);
|
|
22160
24989
|
const filteredIds = limit ? ids.slice(0, limit) : ids;
|
|
22161
24990
|
const results = await this.processPartitionResults(filteredIds, partition, partitionDef, keys);
|
|
22162
|
-
this.
|
|
24991
|
+
this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: results.length, errors: 0 });
|
|
22163
24992
|
return results;
|
|
22164
24993
|
}
|
|
22165
24994
|
/**
|
|
@@ -22204,7 +25033,7 @@ ${errorDetails}`,
|
|
|
22204
25033
|
}
|
|
22205
25034
|
return this.handleResourceError(err, id, context);
|
|
22206
25035
|
});
|
|
22207
|
-
this.
|
|
25036
|
+
this._emitWithDeprecation("list", "listed", { count: results.length, errors: 0 });
|
|
22208
25037
|
return results;
|
|
22209
25038
|
}
|
|
22210
25039
|
/**
|
|
@@ -22267,10 +25096,10 @@ ${errorDetails}`,
|
|
|
22267
25096
|
*/
|
|
22268
25097
|
handleListError(error, { partition, partitionValues }) {
|
|
22269
25098
|
if (error.message.includes("Partition '") && error.message.includes("' not found")) {
|
|
22270
|
-
this.
|
|
25099
|
+
this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
|
|
22271
25100
|
return [];
|
|
22272
25101
|
}
|
|
22273
|
-
this.
|
|
25102
|
+
this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
|
|
22274
25103
|
return [];
|
|
22275
25104
|
}
|
|
22276
25105
|
/**
|
|
@@ -22303,7 +25132,7 @@ ${errorDetails}`,
|
|
|
22303
25132
|
throw err;
|
|
22304
25133
|
});
|
|
22305
25134
|
const finalResults = await this.executeHooks("afterGetMany", results);
|
|
22306
|
-
this.
|
|
25135
|
+
this._emitWithDeprecation("getMany", "fetched-many", ids.length);
|
|
22307
25136
|
return finalResults;
|
|
22308
25137
|
}
|
|
22309
25138
|
/**
|
|
@@ -22389,7 +25218,7 @@ ${errorDetails}`,
|
|
|
22389
25218
|
hasTotalItems: totalItems !== null
|
|
22390
25219
|
}
|
|
22391
25220
|
};
|
|
22392
|
-
this.
|
|
25221
|
+
this._emitWithDeprecation("page", "paginated", result2);
|
|
22393
25222
|
return result2;
|
|
22394
25223
|
});
|
|
22395
25224
|
if (ok) return result;
|
|
@@ -22459,7 +25288,7 @@ ${errorDetails}`,
|
|
|
22459
25288
|
contentType
|
|
22460
25289
|
}));
|
|
22461
25290
|
if (!ok2) throw err2;
|
|
22462
|
-
this.
|
|
25291
|
+
this._emitWithDeprecation("setContent", "content-set", { id, contentType, contentLength: buffer.length }, id);
|
|
22463
25292
|
return updatedData;
|
|
22464
25293
|
}
|
|
22465
25294
|
/**
|
|
@@ -22488,7 +25317,7 @@ ${errorDetails}`,
|
|
|
22488
25317
|
}
|
|
22489
25318
|
const buffer = Buffer.from(await response.Body.transformToByteArray());
|
|
22490
25319
|
const contentType = response.ContentType || null;
|
|
22491
|
-
this.
|
|
25320
|
+
this._emitWithDeprecation("content", "content-fetched", { id, contentLength: buffer.length, contentType }, id);
|
|
22492
25321
|
return {
|
|
22493
25322
|
buffer,
|
|
22494
25323
|
contentType
|
|
@@ -22520,7 +25349,7 @@ ${errorDetails}`,
|
|
|
22520
25349
|
metadata: existingMetadata
|
|
22521
25350
|
}));
|
|
22522
25351
|
if (!ok2) throw err2;
|
|
22523
|
-
this.
|
|
25352
|
+
this._emitWithDeprecation("deleteContent", "content-deleted", id, id);
|
|
22524
25353
|
return response;
|
|
22525
25354
|
}
|
|
22526
25355
|
/**
|
|
@@ -22832,7 +25661,7 @@ ${errorDetails}`,
|
|
|
22832
25661
|
const data = await this.get(id);
|
|
22833
25662
|
data._partition = partitionName;
|
|
22834
25663
|
data._partitionValues = partitionValues;
|
|
22835
|
-
this.
|
|
25664
|
+
this._emitWithDeprecation("getFromPartition", "partition-fetched", data, data.id);
|
|
22836
25665
|
return data;
|
|
22837
25666
|
}
|
|
22838
25667
|
/**
|
|
@@ -23241,7 +26070,7 @@ class Database extends EventEmitter {
|
|
|
23241
26070
|
})();
|
|
23242
26071
|
this.version = "1";
|
|
23243
26072
|
this.s3dbVersion = (() => {
|
|
23244
|
-
const [ok, err, version] = tryFn(() => true ? "13.
|
|
26073
|
+
const [ok, err, version] = tryFn(() => true ? "13.2.1" : "latest");
|
|
23245
26074
|
return ok ? version : "latest";
|
|
23246
26075
|
})();
|
|
23247
26076
|
this._resourcesMap = {};
|
|
@@ -23411,12 +26240,12 @@ class Database extends EventEmitter {
|
|
|
23411
26240
|
}
|
|
23412
26241
|
}
|
|
23413
26242
|
if (definitionChanges.length > 0) {
|
|
23414
|
-
this.emit("
|
|
26243
|
+
this.emit("db:resource-definitions-changed", {
|
|
23415
26244
|
changes: definitionChanges,
|
|
23416
26245
|
metadata: this.savedMetadata
|
|
23417
26246
|
});
|
|
23418
26247
|
}
|
|
23419
|
-
this.emit("connected", /* @__PURE__ */ new Date());
|
|
26248
|
+
this.emit("db:connected", /* @__PURE__ */ new Date());
|
|
23420
26249
|
}
|
|
23421
26250
|
/**
|
|
23422
26251
|
* Detect changes in resource definitions compared to saved metadata
|
|
@@ -23630,7 +26459,7 @@ class Database extends EventEmitter {
|
|
|
23630
26459
|
if (index > -1) {
|
|
23631
26460
|
this.pluginList.splice(index, 1);
|
|
23632
26461
|
}
|
|
23633
|
-
this.emit("plugin
|
|
26462
|
+
this.emit("db:plugin:uninstalled", { name: pluginName, plugin });
|
|
23634
26463
|
}
|
|
23635
26464
|
async uploadMetadataFile() {
|
|
23636
26465
|
const metadata = {
|
|
@@ -23689,7 +26518,7 @@ class Database extends EventEmitter {
|
|
|
23689
26518
|
contentType: "application/json"
|
|
23690
26519
|
});
|
|
23691
26520
|
this.savedMetadata = metadata;
|
|
23692
|
-
this.emit("
|
|
26521
|
+
this.emit("db:metadata-uploaded", metadata);
|
|
23693
26522
|
}
|
|
23694
26523
|
blankMetadataStructure() {
|
|
23695
26524
|
return {
|
|
@@ -23946,7 +26775,7 @@ class Database extends EventEmitter {
|
|
|
23946
26775
|
body: JSON.stringify(metadata, null, 2),
|
|
23947
26776
|
contentType: "application/json"
|
|
23948
26777
|
});
|
|
23949
|
-
this.emit("
|
|
26778
|
+
this.emit("db:metadata-healed", { healingLog, metadata });
|
|
23950
26779
|
if (this.verbose) {
|
|
23951
26780
|
console.warn("S3DB: Successfully uploaded healed metadata");
|
|
23952
26781
|
}
|
|
@@ -24086,7 +26915,7 @@ class Database extends EventEmitter {
|
|
|
24086
26915
|
if (!existingVersionData || existingVersionData.hash !== newHash) {
|
|
24087
26916
|
await this.uploadMetadataFile();
|
|
24088
26917
|
}
|
|
24089
|
-
this.emit("
|
|
26918
|
+
this.emit("db:resource:updated", name);
|
|
24090
26919
|
return existingResource;
|
|
24091
26920
|
}
|
|
24092
26921
|
const existingMetadata = this.savedMetadata?.resources?.[name];
|
|
@@ -24123,7 +26952,7 @@ class Database extends EventEmitter {
|
|
|
24123
26952
|
this._applyMiddlewares(resource, middlewares);
|
|
24124
26953
|
}
|
|
24125
26954
|
await this.uploadMetadataFile();
|
|
24126
|
-
this.emit("
|
|
26955
|
+
this.emit("db:resource:created", name);
|
|
24127
26956
|
return resource;
|
|
24128
26957
|
}
|
|
24129
26958
|
/**
|
|
@@ -24287,7 +27116,7 @@ class Database extends EventEmitter {
|
|
|
24287
27116
|
if (this.client && typeof this.client.removeAllListeners === "function") {
|
|
24288
27117
|
this.client.removeAllListeners();
|
|
24289
27118
|
}
|
|
24290
|
-
await this.emit("disconnected", /* @__PURE__ */ new Date());
|
|
27119
|
+
await this.emit("db:disconnected", /* @__PURE__ */ new Date());
|
|
24291
27120
|
this.removeAllListeners();
|
|
24292
27121
|
if (this._exitListener && typeof process !== "undefined") {
|
|
24293
27122
|
process.off("exit", this._exitListener);
|
|
@@ -24399,7 +27228,7 @@ class Database extends EventEmitter {
|
|
|
24399
27228
|
for (const hook of hooks) {
|
|
24400
27229
|
const [ok, error] = await tryFn(() => hook({ database: this, ...context }));
|
|
24401
27230
|
if (!ok) {
|
|
24402
|
-
this.emit("
|
|
27231
|
+
this.emit("db:hook-error", { event, error, context });
|
|
24403
27232
|
if (this.strictHooks) {
|
|
24404
27233
|
throw new DatabaseError(`Hook execution failed for event '${event}': ${error.message}`, {
|
|
24405
27234
|
event,
|
|
@@ -25975,7 +28804,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
25975
28804
|
if (this.config.verbose) {
|
|
25976
28805
|
console.warn(`[ReplicatorPlugin] Insert event failed for resource ${resource.name}: ${error.message}`);
|
|
25977
28806
|
}
|
|
25978
|
-
this.emit("error", { operation: "insert", error: error.message, resource: resource.name });
|
|
28807
|
+
this.emit("plg:replicator:error", { operation: "insert", error: error.message, resource: resource.name });
|
|
25979
28808
|
}
|
|
25980
28809
|
};
|
|
25981
28810
|
const updateHandler = async (data, beforeData) => {
|
|
@@ -25988,7 +28817,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
25988
28817
|
if (this.config.verbose) {
|
|
25989
28818
|
console.warn(`[ReplicatorPlugin] Update event failed for resource ${resource.name}: ${error.message}`);
|
|
25990
28819
|
}
|
|
25991
|
-
this.emit("error", { operation: "update", error: error.message, resource: resource.name });
|
|
28820
|
+
this.emit("plg:replicator:error", { operation: "update", error: error.message, resource: resource.name });
|
|
25992
28821
|
}
|
|
25993
28822
|
};
|
|
25994
28823
|
const deleteHandler = async (data) => {
|
|
@@ -25999,7 +28828,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
25999
28828
|
if (this.config.verbose) {
|
|
26000
28829
|
console.warn(`[ReplicatorPlugin] Delete event failed for resource ${resource.name}: ${error.message}`);
|
|
26001
28830
|
}
|
|
26002
|
-
this.emit("error", { operation: "delete", error: error.message, resource: resource.name });
|
|
28831
|
+
this.emit("plg:replicator:error", { operation: "delete", error: error.message, resource: resource.name });
|
|
26003
28832
|
}
|
|
26004
28833
|
};
|
|
26005
28834
|
this.eventHandlers.set(resource.name, {
|
|
@@ -26120,7 +28949,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26120
28949
|
if (this.config.verbose) {
|
|
26121
28950
|
console.warn(`[ReplicatorPlugin] Failed to log error for ${resourceName}: ${logError.message}`);
|
|
26122
28951
|
}
|
|
26123
|
-
this.emit("
|
|
28952
|
+
this.emit("plg:replicator:log-error", {
|
|
26124
28953
|
replicator: replicator.name || replicator.id,
|
|
26125
28954
|
resourceName,
|
|
26126
28955
|
operation,
|
|
@@ -26145,7 +28974,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26145
28974
|
() => replicator.replicate(resourceName, operation, data, recordId, beforeData),
|
|
26146
28975
|
this.config.maxRetries
|
|
26147
28976
|
);
|
|
26148
|
-
this.emit("replicated", {
|
|
28977
|
+
this.emit("plg:replicator:replicated", {
|
|
26149
28978
|
replicator: replicator.name || replicator.id,
|
|
26150
28979
|
resourceName,
|
|
26151
28980
|
operation,
|
|
@@ -26161,7 +28990,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26161
28990
|
if (this.config.verbose) {
|
|
26162
28991
|
console.warn(`[ReplicatorPlugin] Replication failed for ${replicator.name || replicator.id} on ${resourceName}: ${error.message}`);
|
|
26163
28992
|
}
|
|
26164
|
-
this.emit("
|
|
28993
|
+
this.emit("plg:replicator:error", {
|
|
26165
28994
|
replicator: replicator.name || replicator.id,
|
|
26166
28995
|
resourceName,
|
|
26167
28996
|
operation,
|
|
@@ -26193,7 +29022,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26193
29022
|
if (this.config.verbose) {
|
|
26194
29023
|
console.warn(`[ReplicatorPlugin] Replicator item processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${err.message}`);
|
|
26195
29024
|
}
|
|
26196
|
-
this.emit("
|
|
29025
|
+
this.emit("plg:replicator:error", {
|
|
26197
29026
|
replicator: replicator.name || replicator.id,
|
|
26198
29027
|
resourceName: item.resourceName,
|
|
26199
29028
|
operation: item.operation,
|
|
@@ -26205,7 +29034,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26205
29034
|
}
|
|
26206
29035
|
return { success: false, error: err.message };
|
|
26207
29036
|
}
|
|
26208
|
-
this.emit("replicated", {
|
|
29037
|
+
this.emit("plg:replicator:replicated", {
|
|
26209
29038
|
replicator: replicator.name || replicator.id,
|
|
26210
29039
|
resourceName: item.resourceName,
|
|
26211
29040
|
operation: item.operation,
|
|
@@ -26221,7 +29050,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26221
29050
|
if (this.config.verbose) {
|
|
26222
29051
|
console.warn(`[ReplicatorPlugin] Wrapper processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${wrapperError.message}`);
|
|
26223
29052
|
}
|
|
26224
|
-
this.emit("
|
|
29053
|
+
this.emit("plg:replicator:error", {
|
|
26225
29054
|
replicator: replicator.name || replicator.id,
|
|
26226
29055
|
resourceName: item.resourceName,
|
|
26227
29056
|
operation: item.operation,
|
|
@@ -26239,7 +29068,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26239
29068
|
async logReplicator(item) {
|
|
26240
29069
|
const logRes = this.replicatorLog || this.database.resources[normalizeResourceName(this.config.replicatorLogResource)];
|
|
26241
29070
|
if (!logRes) {
|
|
26242
|
-
this.emit("replicator
|
|
29071
|
+
this.emit("plg:replicator:log-failed", { error: "replicator log resource not found", item });
|
|
26243
29072
|
return;
|
|
26244
29073
|
}
|
|
26245
29074
|
const logItem = {
|
|
@@ -26257,7 +29086,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26257
29086
|
if (this.config.verbose) {
|
|
26258
29087
|
console.warn(`[ReplicatorPlugin] Failed to log replicator item: ${err.message}`);
|
|
26259
29088
|
}
|
|
26260
|
-
this.emit("replicator
|
|
29089
|
+
this.emit("plg:replicator:log-failed", { error: err, item });
|
|
26261
29090
|
}
|
|
26262
29091
|
}
|
|
26263
29092
|
async updateReplicatorLog(logId, updates) {
|
|
@@ -26269,7 +29098,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26269
29098
|
});
|
|
26270
29099
|
});
|
|
26271
29100
|
if (!ok) {
|
|
26272
|
-
this.emit("replicator
|
|
29101
|
+
this.emit("plg:replicator:update-log-failed", { error: err.message, logId, updates });
|
|
26273
29102
|
}
|
|
26274
29103
|
}
|
|
26275
29104
|
// Utility methods
|
|
@@ -26353,7 +29182,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26353
29182
|
for (const resourceName in this.database.resources) {
|
|
26354
29183
|
if (normalizeResourceName(resourceName) === normalizeResourceName("plg_replicator_logs")) continue;
|
|
26355
29184
|
if (replicator.shouldReplicateResource(resourceName)) {
|
|
26356
|
-
this.emit("replicator
|
|
29185
|
+
this.emit("plg:replicator:sync-resource", { resourceName, replicatorId });
|
|
26357
29186
|
const resource = this.database.resources[resourceName];
|
|
26358
29187
|
let offset = 0;
|
|
26359
29188
|
const pageSize = this.config.batchSize || 100;
|
|
@@ -26369,7 +29198,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26369
29198
|
}
|
|
26370
29199
|
}
|
|
26371
29200
|
}
|
|
26372
|
-
this.emit("replicator
|
|
29201
|
+
this.emit("plg:replicator:sync-completed", { replicatorId, stats: this.stats });
|
|
26373
29202
|
}
|
|
26374
29203
|
async stop() {
|
|
26375
29204
|
const [ok, error] = await tryFn(async () => {
|
|
@@ -26384,7 +29213,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26384
29213
|
if (this.config.verbose) {
|
|
26385
29214
|
console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
|
|
26386
29215
|
}
|
|
26387
|
-
this.emit("
|
|
29216
|
+
this.emit("plg:replicator:stop-error", {
|
|
26388
29217
|
replicator: replicator.name || replicator.id || "unknown",
|
|
26389
29218
|
driver: replicator.driver || "unknown",
|
|
26390
29219
|
error: replicatorError.message
|
|
@@ -26415,7 +29244,7 @@ class ReplicatorPlugin extends Plugin {
|
|
|
26415
29244
|
if (this.config.verbose) {
|
|
26416
29245
|
console.warn(`[ReplicatorPlugin] Failed to stop plugin: ${error.message}`);
|
|
26417
29246
|
}
|
|
26418
|
-
this.emit("
|
|
29247
|
+
this.emit("plg:replicator:plugin-stop-error", {
|
|
26419
29248
|
error: error.message
|
|
26420
29249
|
});
|
|
26421
29250
|
}
|
|
@@ -26572,7 +29401,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26572
29401
|
if (this.config.verbose) {
|
|
26573
29402
|
console.log(`[S3QueuePlugin] Started ${concurrency} workers`);
|
|
26574
29403
|
}
|
|
26575
|
-
this.emit("workers
|
|
29404
|
+
this.emit("plg:s3-queue:workers-started", { concurrency, workerId: this.workerId });
|
|
26576
29405
|
}
|
|
26577
29406
|
async stopProcessing() {
|
|
26578
29407
|
if (!this.isRunning) return;
|
|
@@ -26587,7 +29416,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26587
29416
|
if (this.config.verbose) {
|
|
26588
29417
|
console.log("[S3QueuePlugin] Stopped all workers");
|
|
26589
29418
|
}
|
|
26590
|
-
this.emit("workers
|
|
29419
|
+
this.emit("plg:s3-queue:workers-stopped", { workerId: this.workerId });
|
|
26591
29420
|
}
|
|
26592
29421
|
createWorker(handler, workerIndex) {
|
|
26593
29422
|
return (async () => {
|
|
@@ -26755,7 +29584,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26755
29584
|
});
|
|
26756
29585
|
await this.completeMessage(message.queueId, result);
|
|
26757
29586
|
const duration = Date.now() - startTime;
|
|
26758
|
-
this.emit("message
|
|
29587
|
+
this.emit("plg:s3-queue:message-completed", {
|
|
26759
29588
|
queueId: message.queueId,
|
|
26760
29589
|
originalId: message.record.id,
|
|
26761
29590
|
duration,
|
|
@@ -26768,7 +29597,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26768
29597
|
const shouldRetry = message.attempts < message.maxAttempts;
|
|
26769
29598
|
if (shouldRetry) {
|
|
26770
29599
|
await this.retryMessage(message.queueId, message.attempts, error.message);
|
|
26771
|
-
this.emit("message
|
|
29600
|
+
this.emit("plg:s3-queue:message-retry", {
|
|
26772
29601
|
queueId: message.queueId,
|
|
26773
29602
|
originalId: message.record.id,
|
|
26774
29603
|
attempts: message.attempts,
|
|
@@ -26776,7 +29605,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
26776
29605
|
});
|
|
26777
29606
|
} else {
|
|
26778
29607
|
await this.moveToDeadLetter(message.queueId, message.record, error.message);
|
|
26779
|
-
this.emit("message
|
|
29608
|
+
this.emit("plg:s3-queue:message-dead", {
|
|
26780
29609
|
queueId: message.queueId,
|
|
26781
29610
|
originalId: message.record.id,
|
|
26782
29611
|
error: error.message
|
|
@@ -27008,7 +29837,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27008
29837
|
});
|
|
27009
29838
|
}
|
|
27010
29839
|
await this._startScheduling();
|
|
27011
|
-
this.emit("initialized", { jobs: this.jobs.size });
|
|
29840
|
+
this.emit("db:plugin:initialized", { jobs: this.jobs.size });
|
|
27012
29841
|
}
|
|
27013
29842
|
async _createJobHistoryResource() {
|
|
27014
29843
|
const [ok] = await tryFn(() => this.database.createResource({
|
|
@@ -27146,7 +29975,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27146
29975
|
if (this.config.onJobStart) {
|
|
27147
29976
|
await this._executeHook(this.config.onJobStart, jobName, context);
|
|
27148
29977
|
}
|
|
27149
|
-
this.emit("
|
|
29978
|
+
this.emit("plg:scheduler:job-start", { jobName, executionId, startTime });
|
|
27150
29979
|
let attempt = 0;
|
|
27151
29980
|
let lastError = null;
|
|
27152
29981
|
let result = null;
|
|
@@ -27213,7 +30042,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27213
30042
|
} else if (status !== "success" && this.config.onJobError) {
|
|
27214
30043
|
await this._executeHook(this.config.onJobError, jobName, lastError, attempt);
|
|
27215
30044
|
}
|
|
27216
|
-
this.emit("
|
|
30045
|
+
this.emit("plg:scheduler:job-complete", {
|
|
27217
30046
|
jobName,
|
|
27218
30047
|
executionId,
|
|
27219
30048
|
status,
|
|
@@ -27299,7 +30128,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27299
30128
|
}
|
|
27300
30129
|
job.enabled = true;
|
|
27301
30130
|
this._scheduleNextExecution(jobName);
|
|
27302
|
-
this.emit("
|
|
30131
|
+
this.emit("plg:scheduler:job-enabled", { jobName });
|
|
27303
30132
|
}
|
|
27304
30133
|
/**
|
|
27305
30134
|
* Disable a job
|
|
@@ -27320,7 +30149,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27320
30149
|
clearTimeout(timer);
|
|
27321
30150
|
this.timers.delete(jobName);
|
|
27322
30151
|
}
|
|
27323
|
-
this.emit("
|
|
30152
|
+
this.emit("plg:scheduler:job-disabled", { jobName });
|
|
27324
30153
|
}
|
|
27325
30154
|
/**
|
|
27326
30155
|
* Get job status and statistics
|
|
@@ -27458,7 +30287,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27458
30287
|
if (job.enabled) {
|
|
27459
30288
|
this._scheduleNextExecution(jobName);
|
|
27460
30289
|
}
|
|
27461
|
-
this.emit("
|
|
30290
|
+
this.emit("plg:scheduler:job-added", { jobName });
|
|
27462
30291
|
}
|
|
27463
30292
|
/**
|
|
27464
30293
|
* Remove a job
|
|
@@ -27481,7 +30310,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
27481
30310
|
this.jobs.delete(jobName);
|
|
27482
30311
|
this.statistics.delete(jobName);
|
|
27483
30312
|
this.activeJobs.delete(jobName);
|
|
27484
|
-
this.emit("
|
|
30313
|
+
this.emit("plg:scheduler:job-removed", { jobName });
|
|
27485
30314
|
}
|
|
27486
30315
|
/**
|
|
27487
30316
|
* Get plugin instance by name (for job actions that need other plugins)
|
|
@@ -27522,9 +30351,14 @@ class SchedulerPlugin extends Plugin {
|
|
|
27522
30351
|
}
|
|
27523
30352
|
}
|
|
27524
30353
|
|
|
30354
|
+
var scheduler_plugin = /*#__PURE__*/Object.freeze({
|
|
30355
|
+
__proto__: null,
|
|
30356
|
+
SchedulerPlugin: SchedulerPlugin
|
|
30357
|
+
});
|
|
30358
|
+
|
|
27525
30359
|
class StateMachineError extends S3dbError {
|
|
27526
30360
|
constructor(message, details = {}) {
|
|
27527
|
-
const { currentState, targetState, resourceName, operation = "unknown", ...rest } = details;
|
|
30361
|
+
const { currentState, targetState, resourceName, operation = "unknown", retriable, ...rest } = details;
|
|
27528
30362
|
let description = details.description;
|
|
27529
30363
|
if (!description) {
|
|
27530
30364
|
description = `
|
|
@@ -27549,6 +30383,158 @@ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-mach
|
|
|
27549
30383
|
`.trim();
|
|
27550
30384
|
}
|
|
27551
30385
|
super(message, { ...rest, currentState, targetState, resourceName, operation, description });
|
|
30386
|
+
if (retriable !== void 0) {
|
|
30387
|
+
this.retriable = retriable;
|
|
30388
|
+
}
|
|
30389
|
+
}
|
|
30390
|
+
}
|
|
30391
|
+
|
|
30392
|
+
const RETRIABLE = "RETRIABLE";
|
|
30393
|
+
const NON_RETRIABLE = "NON_RETRIABLE";
|
|
30394
|
+
const RETRIABLE_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
30395
|
+
"ECONNREFUSED",
|
|
30396
|
+
"ETIMEDOUT",
|
|
30397
|
+
"ECONNRESET",
|
|
30398
|
+
"EPIPE",
|
|
30399
|
+
"ENOTFOUND",
|
|
30400
|
+
"NetworkError",
|
|
30401
|
+
"NETWORK_ERROR",
|
|
30402
|
+
"TimeoutError",
|
|
30403
|
+
"TIMEOUT"
|
|
30404
|
+
]);
|
|
30405
|
+
const RETRIABLE_AWS_CODES = /* @__PURE__ */ new Set([
|
|
30406
|
+
"ThrottlingException",
|
|
30407
|
+
"TooManyRequestsException",
|
|
30408
|
+
"RequestLimitExceeded",
|
|
30409
|
+
"ProvisionedThroughputExceededException",
|
|
30410
|
+
"RequestThrottledException",
|
|
30411
|
+
"SlowDown",
|
|
30412
|
+
"ServiceUnavailable"
|
|
30413
|
+
]);
|
|
30414
|
+
const RETRIABLE_AWS_CONFLICTS = /* @__PURE__ */ new Set([
|
|
30415
|
+
"ConditionalCheckFailedException",
|
|
30416
|
+
"TransactionConflictException"
|
|
30417
|
+
]);
|
|
30418
|
+
const RETRIABLE_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
30419
|
+
429,
|
|
30420
|
+
// Too Many Requests
|
|
30421
|
+
500,
|
|
30422
|
+
// Internal Server Error
|
|
30423
|
+
502,
|
|
30424
|
+
// Bad Gateway
|
|
30425
|
+
503,
|
|
30426
|
+
// Service Unavailable
|
|
30427
|
+
504,
|
|
30428
|
+
// Gateway Timeout
|
|
30429
|
+
507,
|
|
30430
|
+
// Insufficient Storage
|
|
30431
|
+
509
|
|
30432
|
+
// Bandwidth Limit Exceeded
|
|
30433
|
+
]);
|
|
30434
|
+
const NON_RETRIABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
30435
|
+
"ValidationError",
|
|
30436
|
+
"StateMachineError",
|
|
30437
|
+
"SchemaError",
|
|
30438
|
+
"AuthenticationError",
|
|
30439
|
+
"PermissionError",
|
|
30440
|
+
"BusinessLogicError",
|
|
30441
|
+
"InvalidStateTransition"
|
|
30442
|
+
]);
|
|
30443
|
+
const NON_RETRIABLE_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
30444
|
+
400,
|
|
30445
|
+
// Bad Request
|
|
30446
|
+
401,
|
|
30447
|
+
// Unauthorized
|
|
30448
|
+
403,
|
|
30449
|
+
// Forbidden
|
|
30450
|
+
404,
|
|
30451
|
+
// Not Found
|
|
30452
|
+
405,
|
|
30453
|
+
// Method Not Allowed
|
|
30454
|
+
406,
|
|
30455
|
+
// Not Acceptable
|
|
30456
|
+
409,
|
|
30457
|
+
// Conflict
|
|
30458
|
+
410,
|
|
30459
|
+
// Gone
|
|
30460
|
+
422
|
|
30461
|
+
// Unprocessable Entity
|
|
30462
|
+
]);
|
|
30463
|
+
class ErrorClassifier {
|
|
30464
|
+
/**
|
|
30465
|
+
* Classify an error as RETRIABLE or NON_RETRIABLE
|
|
30466
|
+
*
|
|
30467
|
+
* @param {Error} error - The error to classify
|
|
30468
|
+
* @param {Object} options - Classification options
|
|
30469
|
+
* @param {Array<string>} options.retryableErrors - Custom retriable error names/codes
|
|
30470
|
+
* @param {Array<string>} options.nonRetriableErrors - Custom non-retriable error names/codes
|
|
30471
|
+
* @returns {string} 'RETRIABLE' or 'NON_RETRIABLE'
|
|
30472
|
+
*/
|
|
30473
|
+
static classify(error, options = {}) {
|
|
30474
|
+
if (!error) return NON_RETRIABLE;
|
|
30475
|
+
const {
|
|
30476
|
+
retryableErrors = [],
|
|
30477
|
+
nonRetriableErrors = []
|
|
30478
|
+
} = options;
|
|
30479
|
+
if (retryableErrors.length > 0) {
|
|
30480
|
+
const isCustomRetriable = retryableErrors.some(
|
|
30481
|
+
(errType) => error.code === errType || error.name === errType || error.message?.includes(errType)
|
|
30482
|
+
);
|
|
30483
|
+
if (isCustomRetriable) return RETRIABLE;
|
|
30484
|
+
}
|
|
30485
|
+
if (nonRetriableErrors.length > 0) {
|
|
30486
|
+
const isCustomNonRetriable = nonRetriableErrors.some(
|
|
30487
|
+
(errType) => error.code === errType || error.name === errType || error.message?.includes(errType)
|
|
30488
|
+
);
|
|
30489
|
+
if (isCustomNonRetriable) return NON_RETRIABLE;
|
|
30490
|
+
}
|
|
30491
|
+
if (error.retriable === false) return NON_RETRIABLE;
|
|
30492
|
+
if (error.retriable === true) return RETRIABLE;
|
|
30493
|
+
if (NON_RETRIABLE_ERROR_NAMES.has(error.name)) {
|
|
30494
|
+
return NON_RETRIABLE;
|
|
30495
|
+
}
|
|
30496
|
+
if (error.statusCode && NON_RETRIABLE_STATUS_CODES.has(error.statusCode)) {
|
|
30497
|
+
return NON_RETRIABLE;
|
|
30498
|
+
}
|
|
30499
|
+
if (error.code && RETRIABLE_NETWORK_CODES.has(error.code)) {
|
|
30500
|
+
return RETRIABLE;
|
|
30501
|
+
}
|
|
30502
|
+
if (error.code && RETRIABLE_AWS_CODES.has(error.code)) {
|
|
30503
|
+
return RETRIABLE;
|
|
30504
|
+
}
|
|
30505
|
+
if (error.code && RETRIABLE_AWS_CONFLICTS.has(error.code)) {
|
|
30506
|
+
return RETRIABLE;
|
|
30507
|
+
}
|
|
30508
|
+
if (error.statusCode && RETRIABLE_STATUS_CODES.has(error.statusCode)) {
|
|
30509
|
+
return RETRIABLE;
|
|
30510
|
+
}
|
|
30511
|
+
if (error.message && typeof error.message === "string") {
|
|
30512
|
+
const lowerMessage = error.message.toLowerCase();
|
|
30513
|
+
if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out") || lowerMessage.includes("network") || lowerMessage.includes("connection")) {
|
|
30514
|
+
return RETRIABLE;
|
|
30515
|
+
}
|
|
30516
|
+
}
|
|
30517
|
+
return RETRIABLE;
|
|
30518
|
+
}
|
|
30519
|
+
/**
|
|
30520
|
+
* Check if an error is retriable
|
|
30521
|
+
*
|
|
30522
|
+
* @param {Error} error - The error to check
|
|
30523
|
+
* @param {Object} options - Classification options
|
|
30524
|
+
* @returns {boolean} true if retriable
|
|
30525
|
+
*/
|
|
30526
|
+
static isRetriable(error, options = {}) {
|
|
30527
|
+
return this.classify(error, options) === RETRIABLE;
|
|
30528
|
+
}
|
|
30529
|
+
/**
|
|
30530
|
+
* Check if an error is non-retriable
|
|
30531
|
+
*
|
|
30532
|
+
* @param {Error} error - The error to check
|
|
30533
|
+
* @param {Object} options - Classification options
|
|
30534
|
+
* @returns {boolean} true if non-retriable
|
|
30535
|
+
*/
|
|
30536
|
+
static isNonRetriable(error, options = {}) {
|
|
30537
|
+
return this.classify(error, options) === NON_RETRIABLE;
|
|
27552
30538
|
}
|
|
27553
30539
|
}
|
|
27554
30540
|
|
|
@@ -27569,11 +30555,23 @@ class StateMachinePlugin extends Plugin {
|
|
|
27569
30555
|
workerId: options.workerId || "default",
|
|
27570
30556
|
lockTimeout: options.lockTimeout || 1e3,
|
|
27571
30557
|
// Wait up to 1s for lock
|
|
27572
|
-
lockTTL: options.lockTTL || 5
|
|
30558
|
+
lockTTL: options.lockTTL || 5,
|
|
27573
30559
|
// Lock expires after 5s (prevent deadlock)
|
|
30560
|
+
// Global retry configuration for action execution
|
|
30561
|
+
retryConfig: options.retryConfig || null,
|
|
30562
|
+
// Trigger system configuration
|
|
30563
|
+
enableScheduler: options.enableScheduler || false,
|
|
30564
|
+
schedulerConfig: options.schedulerConfig || {},
|
|
30565
|
+
enableDateTriggers: options.enableDateTriggers !== false,
|
|
30566
|
+
enableFunctionTriggers: options.enableFunctionTriggers !== false,
|
|
30567
|
+
enableEventTriggers: options.enableEventTriggers !== false,
|
|
30568
|
+
triggerCheckInterval: options.triggerCheckInterval || 6e4
|
|
30569
|
+
// Check triggers every 60s by default
|
|
27574
30570
|
};
|
|
27575
30571
|
this.database = null;
|
|
27576
30572
|
this.machines = /* @__PURE__ */ new Map();
|
|
30573
|
+
this.triggerIntervals = [];
|
|
30574
|
+
this.schedulerPlugin = null;
|
|
27577
30575
|
this._validateConfiguration();
|
|
27578
30576
|
}
|
|
27579
30577
|
_validateConfiguration() {
|
|
@@ -27622,7 +30620,8 @@ class StateMachinePlugin extends Plugin {
|
|
|
27622
30620
|
// entityId -> currentState
|
|
27623
30621
|
});
|
|
27624
30622
|
}
|
|
27625
|
-
|
|
30623
|
+
await this._setupTriggers();
|
|
30624
|
+
this.emit("db:plugin:initialized", { machines: Array.from(this.machines.keys()) });
|
|
27626
30625
|
}
|
|
27627
30626
|
async _createStateResources() {
|
|
27628
30627
|
const [logOk] = await tryFn(() => this.database.createResource({
|
|
@@ -27653,6 +30652,8 @@ class StateMachinePlugin extends Plugin {
|
|
|
27653
30652
|
currentState: "string|required",
|
|
27654
30653
|
context: "json|default:{}",
|
|
27655
30654
|
lastTransition: "string|default:null",
|
|
30655
|
+
triggerCounts: "json|default:{}",
|
|
30656
|
+
// Track trigger execution counts
|
|
27656
30657
|
updatedAt: "string|required"
|
|
27657
30658
|
},
|
|
27658
30659
|
behavior: "body-overflow"
|
|
@@ -27716,7 +30717,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
27716
30717
|
if (targetStateConfig && targetStateConfig.entry) {
|
|
27717
30718
|
await this._executeAction(targetStateConfig.entry, context, event, machineId, entityId);
|
|
27718
30719
|
}
|
|
27719
|
-
this.emit("transition", {
|
|
30720
|
+
this.emit("plg:state-machine:transition", {
|
|
27720
30721
|
machineId,
|
|
27721
30722
|
entityId,
|
|
27722
30723
|
from: currentState,
|
|
@@ -27742,14 +30743,97 @@ class StateMachinePlugin extends Plugin {
|
|
|
27742
30743
|
}
|
|
27743
30744
|
return;
|
|
27744
30745
|
}
|
|
27745
|
-
const
|
|
27746
|
-
|
|
27747
|
-
|
|
27748
|
-
|
|
27749
|
-
|
|
27750
|
-
|
|
30746
|
+
const machine = this.machines.get(machineId);
|
|
30747
|
+
const currentState = await this.getState(machineId, entityId);
|
|
30748
|
+
const stateConfig = machine?.config?.states?.[currentState];
|
|
30749
|
+
const retryConfig = {
|
|
30750
|
+
...this.config.retryConfig || {},
|
|
30751
|
+
...machine?.config?.retryConfig || {},
|
|
30752
|
+
...stateConfig?.retryConfig || {}
|
|
30753
|
+
};
|
|
30754
|
+
const maxAttempts = retryConfig.maxAttempts ?? 0;
|
|
30755
|
+
const retryEnabled = maxAttempts > 0;
|
|
30756
|
+
let attempt = 0;
|
|
30757
|
+
while (attempt <= maxAttempts) {
|
|
30758
|
+
try {
|
|
30759
|
+
const result = await action(context, event, { database: this.database, machineId, entityId });
|
|
30760
|
+
if (attempt > 0) {
|
|
30761
|
+
this.emit("plg:state-machine:action-retry-success", {
|
|
30762
|
+
machineId,
|
|
30763
|
+
entityId,
|
|
30764
|
+
action: actionName,
|
|
30765
|
+
attempts: attempt + 1,
|
|
30766
|
+
state: currentState
|
|
30767
|
+
});
|
|
30768
|
+
if (this.config.verbose) {
|
|
30769
|
+
console.log(`[StateMachinePlugin] Action '${actionName}' succeeded after ${attempt + 1} attempts`);
|
|
30770
|
+
}
|
|
30771
|
+
}
|
|
30772
|
+
return result;
|
|
30773
|
+
} catch (error) {
|
|
30774
|
+
if (!retryEnabled) {
|
|
30775
|
+
if (this.config.verbose) {
|
|
30776
|
+
console.error(`[StateMachinePlugin] Action '${actionName}' failed:`, error.message);
|
|
30777
|
+
}
|
|
30778
|
+
this.emit("plg:state-machine:action-error", { actionName, error: error.message, machineId, entityId });
|
|
30779
|
+
return;
|
|
30780
|
+
}
|
|
30781
|
+
const classification = ErrorClassifier.classify(error, {
|
|
30782
|
+
retryableErrors: retryConfig.retryableErrors,
|
|
30783
|
+
nonRetriableErrors: retryConfig.nonRetriableErrors
|
|
30784
|
+
});
|
|
30785
|
+
if (classification === "NON_RETRIABLE") {
|
|
30786
|
+
this.emit("plg:state-machine:action-error-non-retriable", {
|
|
30787
|
+
machineId,
|
|
30788
|
+
entityId,
|
|
30789
|
+
action: actionName,
|
|
30790
|
+
error: error.message,
|
|
30791
|
+
state: currentState
|
|
30792
|
+
});
|
|
30793
|
+
if (this.config.verbose) {
|
|
30794
|
+
console.error(`[StateMachinePlugin] Action '${actionName}' failed with non-retriable error:`, error.message);
|
|
30795
|
+
}
|
|
30796
|
+
throw error;
|
|
30797
|
+
}
|
|
30798
|
+
if (attempt >= maxAttempts) {
|
|
30799
|
+
this.emit("plg:state-machine:action-retry-exhausted", {
|
|
30800
|
+
machineId,
|
|
30801
|
+
entityId,
|
|
30802
|
+
action: actionName,
|
|
30803
|
+
attempts: attempt + 1,
|
|
30804
|
+
error: error.message,
|
|
30805
|
+
state: currentState
|
|
30806
|
+
});
|
|
30807
|
+
if (this.config.verbose) {
|
|
30808
|
+
console.error(`[StateMachinePlugin] Action '${actionName}' failed after ${attempt + 1} attempts:`, error.message);
|
|
30809
|
+
}
|
|
30810
|
+
throw error;
|
|
30811
|
+
}
|
|
30812
|
+
attempt++;
|
|
30813
|
+
const delay = this._calculateBackoff(attempt, retryConfig);
|
|
30814
|
+
if (retryConfig.onRetry) {
|
|
30815
|
+
try {
|
|
30816
|
+
await retryConfig.onRetry(attempt, error, context);
|
|
30817
|
+
} catch (hookError) {
|
|
30818
|
+
if (this.config.verbose) {
|
|
30819
|
+
console.warn(`[StateMachinePlugin] onRetry hook failed:`, hookError.message);
|
|
30820
|
+
}
|
|
30821
|
+
}
|
|
30822
|
+
}
|
|
30823
|
+
this.emit("plg:state-machine:action-retry-attempt", {
|
|
30824
|
+
machineId,
|
|
30825
|
+
entityId,
|
|
30826
|
+
action: actionName,
|
|
30827
|
+
attempt,
|
|
30828
|
+
delay,
|
|
30829
|
+
error: error.message,
|
|
30830
|
+
state: currentState
|
|
30831
|
+
});
|
|
30832
|
+
if (this.config.verbose) {
|
|
30833
|
+
console.warn(`[StateMachinePlugin] Action '${actionName}' failed (attempt ${attempt + 1}/${maxAttempts + 1}), retrying in ${delay}ms:`, error.message);
|
|
30834
|
+
}
|
|
30835
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
27751
30836
|
}
|
|
27752
|
-
this.emit("action_error", { actionName, error: error.message, machineId, entityId });
|
|
27753
30837
|
}
|
|
27754
30838
|
}
|
|
27755
30839
|
async _transition(machineId, entityId, fromState, toState, event, context) {
|
|
@@ -27847,6 +30931,27 @@ class StateMachinePlugin extends Plugin {
|
|
|
27847
30931
|
console.warn(`[StateMachinePlugin] Failed to release lock '${lockName}':`, err.message);
|
|
27848
30932
|
}
|
|
27849
30933
|
}
|
|
30934
|
+
/**
|
|
30935
|
+
* Calculate backoff delay for retry attempts
|
|
30936
|
+
* @private
|
|
30937
|
+
*/
|
|
30938
|
+
_calculateBackoff(attempt, retryConfig) {
|
|
30939
|
+
const {
|
|
30940
|
+
backoffStrategy = "exponential",
|
|
30941
|
+
baseDelay = 1e3,
|
|
30942
|
+
maxDelay = 3e4
|
|
30943
|
+
} = retryConfig || {};
|
|
30944
|
+
let delay;
|
|
30945
|
+
if (backoffStrategy === "exponential") {
|
|
30946
|
+
delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
30947
|
+
} else if (backoffStrategy === "linear") {
|
|
30948
|
+
delay = Math.min(baseDelay * attempt, maxDelay);
|
|
30949
|
+
} else {
|
|
30950
|
+
delay = baseDelay;
|
|
30951
|
+
}
|
|
30952
|
+
const jitter = delay * 0.2 * (Math.random() - 0.5);
|
|
30953
|
+
return Math.round(delay + jitter);
|
|
30954
|
+
}
|
|
27850
30955
|
/**
|
|
27851
30956
|
* Get current state for an entity
|
|
27852
30957
|
*/
|
|
@@ -27976,7 +31081,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
27976
31081
|
if (initialStateConfig && initialStateConfig.entry) {
|
|
27977
31082
|
await this._executeAction(initialStateConfig.entry, context, "INIT", machineId, entityId);
|
|
27978
31083
|
}
|
|
27979
|
-
this.emit("
|
|
31084
|
+
this.emit("plg:state-machine:entity-initialized", { machineId, entityId, initialState });
|
|
27980
31085
|
return initialState;
|
|
27981
31086
|
}
|
|
27982
31087
|
/**
|
|
@@ -28033,12 +31138,343 @@ class StateMachinePlugin extends Plugin {
|
|
|
28033
31138
|
`;
|
|
28034
31139
|
return dot;
|
|
28035
31140
|
}
|
|
31141
|
+
/**
|
|
31142
|
+
* Get all entities currently in a specific state
|
|
31143
|
+
* @private
|
|
31144
|
+
*/
|
|
31145
|
+
async _getEntitiesInState(machineId, stateName) {
|
|
31146
|
+
if (!this.config.persistTransitions) {
|
|
31147
|
+
const machine = this.machines.get(machineId);
|
|
31148
|
+
if (!machine) return [];
|
|
31149
|
+
const entities = [];
|
|
31150
|
+
for (const [entityId, currentState] of machine.currentStates) {
|
|
31151
|
+
if (currentState === stateName) {
|
|
31152
|
+
entities.push({ entityId, currentState, context: {}, triggerCounts: {} });
|
|
31153
|
+
}
|
|
31154
|
+
}
|
|
31155
|
+
return entities;
|
|
31156
|
+
}
|
|
31157
|
+
const [ok, err, records] = await tryFn(
|
|
31158
|
+
() => this.database.resources[this.config.stateResource].query({
|
|
31159
|
+
machineId,
|
|
31160
|
+
currentState: stateName
|
|
31161
|
+
})
|
|
31162
|
+
);
|
|
31163
|
+
if (!ok) {
|
|
31164
|
+
if (this.config.verbose) {
|
|
31165
|
+
console.warn(`[StateMachinePlugin] Failed to query entities in state '${stateName}':`, err.message);
|
|
31166
|
+
}
|
|
31167
|
+
return [];
|
|
31168
|
+
}
|
|
31169
|
+
return records || [];
|
|
31170
|
+
}
|
|
31171
|
+
/**
|
|
31172
|
+
* Increment trigger execution count for an entity
|
|
31173
|
+
* @private
|
|
31174
|
+
*/
|
|
31175
|
+
async _incrementTriggerCount(machineId, entityId, triggerName) {
|
|
31176
|
+
if (!this.config.persistTransitions) {
|
|
31177
|
+
return;
|
|
31178
|
+
}
|
|
31179
|
+
const stateId = `${machineId}_${entityId}`;
|
|
31180
|
+
const [ok, err, stateRecord] = await tryFn(
|
|
31181
|
+
() => this.database.resources[this.config.stateResource].get(stateId)
|
|
31182
|
+
);
|
|
31183
|
+
if (ok && stateRecord) {
|
|
31184
|
+
const triggerCounts = stateRecord.triggerCounts || {};
|
|
31185
|
+
triggerCounts[triggerName] = (triggerCounts[triggerName] || 0) + 1;
|
|
31186
|
+
await tryFn(
|
|
31187
|
+
() => this.database.resources[this.config.stateResource].patch(stateId, { triggerCounts })
|
|
31188
|
+
);
|
|
31189
|
+
}
|
|
31190
|
+
}
|
|
31191
|
+
/**
|
|
31192
|
+
* Setup trigger system for all state machines
|
|
31193
|
+
* @private
|
|
31194
|
+
*/
|
|
31195
|
+
async _setupTriggers() {
|
|
31196
|
+
if (!this.config.enableScheduler && !this.config.enableDateTriggers && !this.config.enableFunctionTriggers && !this.config.enableEventTriggers) {
|
|
31197
|
+
return;
|
|
31198
|
+
}
|
|
31199
|
+
const cronJobs = {};
|
|
31200
|
+
for (const [machineId, machineData] of this.machines) {
|
|
31201
|
+
const machineConfig = machineData.config;
|
|
31202
|
+
for (const [stateName, stateConfig] of Object.entries(machineConfig.states)) {
|
|
31203
|
+
const triggers = stateConfig.triggers || [];
|
|
31204
|
+
for (let i = 0; i < triggers.length; i++) {
|
|
31205
|
+
const trigger = triggers[i];
|
|
31206
|
+
const triggerName = `${trigger.action}_${i}`;
|
|
31207
|
+
if (trigger.type === "cron" && this.config.enableScheduler) {
|
|
31208
|
+
const jobName = `${machineId}_${stateName}_${triggerName}`;
|
|
31209
|
+
cronJobs[jobName] = await this._createCronJob(machineId, stateName, trigger, triggerName);
|
|
31210
|
+
} else if (trigger.type === "date" && this.config.enableDateTriggers) {
|
|
31211
|
+
await this._setupDateTrigger(machineId, stateName, trigger, triggerName);
|
|
31212
|
+
} else if (trigger.type === "function" && this.config.enableFunctionTriggers) {
|
|
31213
|
+
await this._setupFunctionTrigger(machineId, stateName, trigger, triggerName);
|
|
31214
|
+
} else if (trigger.type === "event" && this.config.enableEventTriggers) {
|
|
31215
|
+
await this._setupEventTrigger(machineId, stateName, trigger, triggerName);
|
|
31216
|
+
}
|
|
31217
|
+
}
|
|
31218
|
+
}
|
|
31219
|
+
}
|
|
31220
|
+
if (Object.keys(cronJobs).length > 0 && this.config.enableScheduler) {
|
|
31221
|
+
const { SchedulerPlugin } = await Promise.resolve().then(function () { return scheduler_plugin; });
|
|
31222
|
+
this.schedulerPlugin = new SchedulerPlugin({
|
|
31223
|
+
jobs: cronJobs,
|
|
31224
|
+
persistJobs: false,
|
|
31225
|
+
// Don't persist trigger jobs
|
|
31226
|
+
verbose: this.config.verbose,
|
|
31227
|
+
...this.config.schedulerConfig
|
|
31228
|
+
});
|
|
31229
|
+
await this.database.usePlugin(this.schedulerPlugin);
|
|
31230
|
+
if (this.config.verbose) {
|
|
31231
|
+
console.log(`[StateMachinePlugin] Installed SchedulerPlugin with ${Object.keys(cronJobs).length} cron triggers`);
|
|
31232
|
+
}
|
|
31233
|
+
}
|
|
31234
|
+
}
|
|
31235
|
+
/**
|
|
31236
|
+
* Create a SchedulerPlugin job for a cron trigger
|
|
31237
|
+
* @private
|
|
31238
|
+
*/
|
|
31239
|
+
async _createCronJob(machineId, stateName, trigger, triggerName) {
|
|
31240
|
+
return {
|
|
31241
|
+
schedule: trigger.schedule,
|
|
31242
|
+
description: `Trigger '${triggerName}' for ${machineId}.${stateName}`,
|
|
31243
|
+
action: async (database, context) => {
|
|
31244
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
31245
|
+
let executedCount = 0;
|
|
31246
|
+
for (const entity of entities) {
|
|
31247
|
+
try {
|
|
31248
|
+
if (trigger.condition) {
|
|
31249
|
+
const shouldTrigger = await trigger.condition(entity.context, entity.entityId);
|
|
31250
|
+
if (!shouldTrigger) continue;
|
|
31251
|
+
}
|
|
31252
|
+
if (trigger.maxTriggers !== void 0) {
|
|
31253
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
31254
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
31255
|
+
if (trigger.onMaxTriggersReached) {
|
|
31256
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
31257
|
+
}
|
|
31258
|
+
continue;
|
|
31259
|
+
}
|
|
31260
|
+
}
|
|
31261
|
+
const result = await this._executeAction(
|
|
31262
|
+
trigger.action,
|
|
31263
|
+
entity.context,
|
|
31264
|
+
"TRIGGER",
|
|
31265
|
+
machineId,
|
|
31266
|
+
entity.entityId
|
|
31267
|
+
);
|
|
31268
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
31269
|
+
executedCount++;
|
|
31270
|
+
if (trigger.eventOnSuccess) {
|
|
31271
|
+
await this.send(machineId, entity.entityId, trigger.eventOnSuccess, {
|
|
31272
|
+
...entity.context,
|
|
31273
|
+
triggerResult: result
|
|
31274
|
+
});
|
|
31275
|
+
} else if (trigger.event) {
|
|
31276
|
+
await this.send(machineId, entity.entityId, trigger.event, {
|
|
31277
|
+
...entity.context,
|
|
31278
|
+
triggerResult: result
|
|
31279
|
+
});
|
|
31280
|
+
}
|
|
31281
|
+
this.emit("plg:state-machine:trigger-executed", {
|
|
31282
|
+
machineId,
|
|
31283
|
+
entityId: entity.entityId,
|
|
31284
|
+
state: stateName,
|
|
31285
|
+
trigger: triggerName,
|
|
31286
|
+
type: "cron"
|
|
31287
|
+
});
|
|
31288
|
+
} catch (error) {
|
|
31289
|
+
if (trigger.event) {
|
|
31290
|
+
await tryFn(() => this.send(machineId, entity.entityId, trigger.event, {
|
|
31291
|
+
...entity.context,
|
|
31292
|
+
triggerError: error.message
|
|
31293
|
+
}));
|
|
31294
|
+
}
|
|
31295
|
+
if (this.config.verbose) {
|
|
31296
|
+
console.error(`[StateMachinePlugin] Trigger '${triggerName}' failed for entity ${entity.entityId}:`, error.message);
|
|
31297
|
+
}
|
|
31298
|
+
}
|
|
31299
|
+
}
|
|
31300
|
+
return { processed: entities.length, executed: executedCount };
|
|
31301
|
+
}
|
|
31302
|
+
};
|
|
31303
|
+
}
|
|
31304
|
+
/**
|
|
31305
|
+
* Setup a date-based trigger
|
|
31306
|
+
* @private
|
|
31307
|
+
*/
|
|
31308
|
+
async _setupDateTrigger(machineId, stateName, trigger, triggerName) {
|
|
31309
|
+
const checkInterval = setInterval(async () => {
|
|
31310
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
31311
|
+
for (const entity of entities) {
|
|
31312
|
+
try {
|
|
31313
|
+
const triggerDateValue = entity.context?.[trigger.field];
|
|
31314
|
+
if (!triggerDateValue) continue;
|
|
31315
|
+
const triggerDate = new Date(triggerDateValue);
|
|
31316
|
+
const now = /* @__PURE__ */ new Date();
|
|
31317
|
+
if (now >= triggerDate) {
|
|
31318
|
+
if (trigger.maxTriggers !== void 0) {
|
|
31319
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
31320
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
31321
|
+
if (trigger.onMaxTriggersReached) {
|
|
31322
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
31323
|
+
}
|
|
31324
|
+
continue;
|
|
31325
|
+
}
|
|
31326
|
+
}
|
|
31327
|
+
const result = await this._executeAction(trigger.action, entity.context, "TRIGGER", machineId, entity.entityId);
|
|
31328
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
31329
|
+
if (trigger.event) {
|
|
31330
|
+
await this.send(machineId, entity.entityId, trigger.event, {
|
|
31331
|
+
...entity.context,
|
|
31332
|
+
triggerResult: result
|
|
31333
|
+
});
|
|
31334
|
+
}
|
|
31335
|
+
this.emit("plg:state-machine:trigger-executed", {
|
|
31336
|
+
machineId,
|
|
31337
|
+
entityId: entity.entityId,
|
|
31338
|
+
state: stateName,
|
|
31339
|
+
trigger: triggerName,
|
|
31340
|
+
type: "date"
|
|
31341
|
+
});
|
|
31342
|
+
}
|
|
31343
|
+
} catch (error) {
|
|
31344
|
+
if (this.config.verbose) {
|
|
31345
|
+
console.error(`[StateMachinePlugin] Date trigger '${triggerName}' failed:`, error.message);
|
|
31346
|
+
}
|
|
31347
|
+
}
|
|
31348
|
+
}
|
|
31349
|
+
}, this.config.triggerCheckInterval);
|
|
31350
|
+
this.triggerIntervals.push(checkInterval);
|
|
31351
|
+
}
|
|
31352
|
+
/**
|
|
31353
|
+
* Setup a function-based trigger
|
|
31354
|
+
* @private
|
|
31355
|
+
*/
|
|
31356
|
+
async _setupFunctionTrigger(machineId, stateName, trigger, triggerName) {
|
|
31357
|
+
const interval = trigger.interval || this.config.triggerCheckInterval;
|
|
31358
|
+
const checkInterval = setInterval(async () => {
|
|
31359
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
31360
|
+
for (const entity of entities) {
|
|
31361
|
+
try {
|
|
31362
|
+
if (trigger.maxTriggers !== void 0) {
|
|
31363
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
31364
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
31365
|
+
if (trigger.onMaxTriggersReached) {
|
|
31366
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
31367
|
+
}
|
|
31368
|
+
continue;
|
|
31369
|
+
}
|
|
31370
|
+
}
|
|
31371
|
+
const shouldTrigger = await trigger.condition(entity.context, entity.entityId);
|
|
31372
|
+
if (shouldTrigger) {
|
|
31373
|
+
const result = await this._executeAction(trigger.action, entity.context, "TRIGGER", machineId, entity.entityId);
|
|
31374
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
31375
|
+
if (trigger.event) {
|
|
31376
|
+
await this.send(machineId, entity.entityId, trigger.event, {
|
|
31377
|
+
...entity.context,
|
|
31378
|
+
triggerResult: result
|
|
31379
|
+
});
|
|
31380
|
+
}
|
|
31381
|
+
this.emit("plg:state-machine:trigger-executed", {
|
|
31382
|
+
machineId,
|
|
31383
|
+
entityId: entity.entityId,
|
|
31384
|
+
state: stateName,
|
|
31385
|
+
trigger: triggerName,
|
|
31386
|
+
type: "function"
|
|
31387
|
+
});
|
|
31388
|
+
}
|
|
31389
|
+
} catch (error) {
|
|
31390
|
+
if (this.config.verbose) {
|
|
31391
|
+
console.error(`[StateMachinePlugin] Function trigger '${triggerName}' failed:`, error.message);
|
|
31392
|
+
}
|
|
31393
|
+
}
|
|
31394
|
+
}
|
|
31395
|
+
}, interval);
|
|
31396
|
+
this.triggerIntervals.push(checkInterval);
|
|
31397
|
+
}
|
|
31398
|
+
/**
|
|
31399
|
+
* Setup an event-based trigger
|
|
31400
|
+
* @private
|
|
31401
|
+
*/
|
|
31402
|
+
async _setupEventTrigger(machineId, stateName, trigger, triggerName) {
|
|
31403
|
+
const eventName = trigger.event;
|
|
31404
|
+
const eventHandler = async (eventData) => {
|
|
31405
|
+
const entities = await this._getEntitiesInState(machineId, stateName);
|
|
31406
|
+
for (const entity of entities) {
|
|
31407
|
+
try {
|
|
31408
|
+
if (trigger.condition) {
|
|
31409
|
+
const shouldTrigger = await trigger.condition(entity.context, entity.entityId, eventData);
|
|
31410
|
+
if (!shouldTrigger) continue;
|
|
31411
|
+
}
|
|
31412
|
+
if (trigger.maxTriggers !== void 0) {
|
|
31413
|
+
const triggerCount = entity.triggerCounts?.[triggerName] || 0;
|
|
31414
|
+
if (triggerCount >= trigger.maxTriggers) {
|
|
31415
|
+
if (trigger.onMaxTriggersReached) {
|
|
31416
|
+
await this.send(machineId, entity.entityId, trigger.onMaxTriggersReached, entity.context);
|
|
31417
|
+
}
|
|
31418
|
+
continue;
|
|
31419
|
+
}
|
|
31420
|
+
}
|
|
31421
|
+
const result = await this._executeAction(
|
|
31422
|
+
trigger.action,
|
|
31423
|
+
{ ...entity.context, eventData },
|
|
31424
|
+
"TRIGGER",
|
|
31425
|
+
machineId,
|
|
31426
|
+
entity.entityId
|
|
31427
|
+
);
|
|
31428
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
31429
|
+
if (trigger.sendEvent) {
|
|
31430
|
+
await this.send(machineId, entity.entityId, trigger.sendEvent, {
|
|
31431
|
+
...entity.context,
|
|
31432
|
+
triggerResult: result,
|
|
31433
|
+
eventData
|
|
31434
|
+
});
|
|
31435
|
+
}
|
|
31436
|
+
this.emit("plg:state-machine:trigger-executed", {
|
|
31437
|
+
machineId,
|
|
31438
|
+
entityId: entity.entityId,
|
|
31439
|
+
state: stateName,
|
|
31440
|
+
trigger: triggerName,
|
|
31441
|
+
type: "event",
|
|
31442
|
+
eventName
|
|
31443
|
+
});
|
|
31444
|
+
} catch (error) {
|
|
31445
|
+
if (this.config.verbose) {
|
|
31446
|
+
console.error(`[StateMachinePlugin] Event trigger '${triggerName}' failed:`, error.message);
|
|
31447
|
+
}
|
|
31448
|
+
}
|
|
31449
|
+
}
|
|
31450
|
+
};
|
|
31451
|
+
if (eventName.startsWith("db:")) {
|
|
31452
|
+
const dbEventName = eventName.substring(3);
|
|
31453
|
+
this.database.on(dbEventName, eventHandler);
|
|
31454
|
+
if (this.config.verbose) {
|
|
31455
|
+
console.log(`[StateMachinePlugin] Listening to database event '${dbEventName}' for trigger '${triggerName}'`);
|
|
31456
|
+
}
|
|
31457
|
+
} else {
|
|
31458
|
+
this.on(eventName, eventHandler);
|
|
31459
|
+
if (this.config.verbose) {
|
|
31460
|
+
console.log(`[StateMachinePlugin] Listening to plugin event '${eventName}' for trigger '${triggerName}'`);
|
|
31461
|
+
}
|
|
31462
|
+
}
|
|
31463
|
+
}
|
|
28036
31464
|
async start() {
|
|
28037
31465
|
if (this.config.verbose) {
|
|
28038
31466
|
console.log(`[StateMachinePlugin] Started with ${this.machines.size} state machines`);
|
|
28039
31467
|
}
|
|
28040
31468
|
}
|
|
28041
31469
|
async stop() {
|
|
31470
|
+
for (const interval of this.triggerIntervals) {
|
|
31471
|
+
clearInterval(interval);
|
|
31472
|
+
}
|
|
31473
|
+
this.triggerIntervals = [];
|
|
31474
|
+
if (this.schedulerPlugin) {
|
|
31475
|
+
await this.schedulerPlugin.stop();
|
|
31476
|
+
this.schedulerPlugin = null;
|
|
31477
|
+
}
|
|
28042
31478
|
this.machines.clear();
|
|
28043
31479
|
this.removeAllListeners();
|
|
28044
31480
|
}
|
|
@@ -38180,11 +41616,21 @@ class TfStatePlugin extends Plugin {
|
|
|
38180
41616
|
}
|
|
38181
41617
|
}
|
|
38182
41618
|
|
|
41619
|
+
const ONE_HOUR_SEC = 3600;
|
|
41620
|
+
const ONE_DAY_SEC = 86400;
|
|
41621
|
+
const THIRTY_DAYS_SEC = 2592e3;
|
|
41622
|
+
const TEN_SECONDS_MS = 1e4;
|
|
41623
|
+
const ONE_MINUTE_MS = 6e4;
|
|
41624
|
+
const TEN_MINUTES_MS = 6e5;
|
|
41625
|
+
const ONE_HOUR_MS = 36e5;
|
|
41626
|
+
const ONE_DAY_MS = 864e5;
|
|
41627
|
+
const ONE_WEEK_MS = 6048e5;
|
|
41628
|
+
const SECONDS_TO_MS = 1e3;
|
|
38183
41629
|
const GRANULARITIES = {
|
|
38184
41630
|
minute: {
|
|
38185
|
-
threshold:
|
|
41631
|
+
threshold: ONE_HOUR_SEC,
|
|
38186
41632
|
// TTL < 1 hour
|
|
38187
|
-
interval:
|
|
41633
|
+
interval: TEN_SECONDS_MS,
|
|
38188
41634
|
// Check every 10 seconds
|
|
38189
41635
|
cohortsToCheck: 3,
|
|
38190
41636
|
// Check last 3 minutes
|
|
@@ -38192,9 +41638,9 @@ const GRANULARITIES = {
|
|
|
38192
41638
|
// '2024-10-25T14:30'
|
|
38193
41639
|
},
|
|
38194
41640
|
hour: {
|
|
38195
|
-
threshold:
|
|
41641
|
+
threshold: ONE_DAY_SEC,
|
|
38196
41642
|
// TTL < 24 hours
|
|
38197
|
-
interval:
|
|
41643
|
+
interval: TEN_MINUTES_MS,
|
|
38198
41644
|
// Check every 10 minutes
|
|
38199
41645
|
cohortsToCheck: 2,
|
|
38200
41646
|
// Check last 2 hours
|
|
@@ -38202,9 +41648,9 @@ const GRANULARITIES = {
|
|
|
38202
41648
|
// '2024-10-25T14'
|
|
38203
41649
|
},
|
|
38204
41650
|
day: {
|
|
38205
|
-
threshold:
|
|
41651
|
+
threshold: THIRTY_DAYS_SEC,
|
|
38206
41652
|
// TTL < 30 days
|
|
38207
|
-
interval:
|
|
41653
|
+
interval: ONE_HOUR_MS,
|
|
38208
41654
|
// Check every 1 hour
|
|
38209
41655
|
cohortsToCheck: 2,
|
|
38210
41656
|
// Check last 2 days
|
|
@@ -38214,7 +41660,7 @@ const GRANULARITIES = {
|
|
|
38214
41660
|
week: {
|
|
38215
41661
|
threshold: Infinity,
|
|
38216
41662
|
// TTL >= 30 days
|
|
38217
|
-
interval:
|
|
41663
|
+
interval: ONE_DAY_MS,
|
|
38218
41664
|
// Check every 24 hours
|
|
38219
41665
|
cohortsToCheck: 2,
|
|
38220
41666
|
// Check last 2 weeks
|
|
@@ -38230,7 +41676,7 @@ function getWeekNumber(date) {
|
|
|
38230
41676
|
const dayNum = d.getUTCDay() || 7;
|
|
38231
41677
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
38232
41678
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
38233
|
-
return Math.ceil(((d - yearStart) /
|
|
41679
|
+
return Math.ceil(((d - yearStart) / ONE_DAY_MS + 1) / 7);
|
|
38234
41680
|
}
|
|
38235
41681
|
function detectGranularity(ttl) {
|
|
38236
41682
|
if (!ttl) return "day";
|
|
@@ -38247,16 +41693,16 @@ function getExpiredCohorts(granularity, count) {
|
|
|
38247
41693
|
let checkDate;
|
|
38248
41694
|
switch (granularity) {
|
|
38249
41695
|
case "minute":
|
|
38250
|
-
checkDate = new Date(now.getTime() - i *
|
|
41696
|
+
checkDate = new Date(now.getTime() - i * ONE_MINUTE_MS);
|
|
38251
41697
|
break;
|
|
38252
41698
|
case "hour":
|
|
38253
|
-
checkDate = new Date(now.getTime() - i *
|
|
41699
|
+
checkDate = new Date(now.getTime() - i * ONE_HOUR_MS);
|
|
38254
41700
|
break;
|
|
38255
41701
|
case "day":
|
|
38256
|
-
checkDate = new Date(now.getTime() - i *
|
|
41702
|
+
checkDate = new Date(now.getTime() - i * ONE_DAY_MS);
|
|
38257
41703
|
break;
|
|
38258
41704
|
case "week":
|
|
38259
|
-
checkDate = new Date(now.getTime() - i *
|
|
41705
|
+
checkDate = new Date(now.getTime() - i * ONE_WEEK_MS);
|
|
38260
41706
|
break;
|
|
38261
41707
|
}
|
|
38262
41708
|
cohorts.push(config.cohortFormat(checkDate));
|
|
@@ -38300,7 +41746,7 @@ class TTLPlugin extends Plugin {
|
|
|
38300
41746
|
if (this.verbose) {
|
|
38301
41747
|
console.log(`[TTLPlugin] Installed with ${Object.keys(this.resources).length} resources`);
|
|
38302
41748
|
}
|
|
38303
|
-
this.emit("installed", {
|
|
41749
|
+
this.emit("db:plugin:installed", {
|
|
38304
41750
|
plugin: "TTLPlugin",
|
|
38305
41751
|
resources: Object.keys(this.resources)
|
|
38306
41752
|
});
|
|
@@ -38422,7 +41868,7 @@ class TTLPlugin extends Plugin {
|
|
|
38422
41868
|
return;
|
|
38423
41869
|
}
|
|
38424
41870
|
const baseTimestamp = typeof baseTime === "number" ? baseTime : new Date(baseTime).getTime();
|
|
38425
|
-
const expiresAt = config.ttl ? new Date(baseTimestamp + config.ttl *
|
|
41871
|
+
const expiresAt = config.ttl ? new Date(baseTimestamp + config.ttl * SECONDS_TO_MS) : new Date(baseTimestamp);
|
|
38426
41872
|
const cohortConfig = GRANULARITIES[config.granularity];
|
|
38427
41873
|
const cohort = cohortConfig.cohortFormat(expiresAt);
|
|
38428
41874
|
const indexId = `${resourceName}:${record.id}`;
|
|
@@ -38537,7 +41983,7 @@ class TTLPlugin extends Plugin {
|
|
|
38537
41983
|
}
|
|
38538
41984
|
this.stats.lastScanAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38539
41985
|
this.stats.lastScanDuration = Date.now() - startTime;
|
|
38540
|
-
this.emit("
|
|
41986
|
+
this.emit("plg:ttl:scan-completed", {
|
|
38541
41987
|
granularity,
|
|
38542
41988
|
duration: this.stats.lastScanDuration,
|
|
38543
41989
|
cohorts
|
|
@@ -38545,7 +41991,7 @@ class TTLPlugin extends Plugin {
|
|
|
38545
41991
|
} catch (error) {
|
|
38546
41992
|
console.error(`[TTLPlugin] Error in ${granularity} cleanup:`, error);
|
|
38547
41993
|
this.stats.totalErrors++;
|
|
38548
|
-
this.emit("
|
|
41994
|
+
this.emit("plg:ttl:cleanup-error", { granularity, error });
|
|
38549
41995
|
}
|
|
38550
41996
|
}
|
|
38551
41997
|
/**
|
|
@@ -38593,7 +42039,7 @@ class TTLPlugin extends Plugin {
|
|
|
38593
42039
|
}
|
|
38594
42040
|
await this.expirationIndex.delete(entry.id);
|
|
38595
42041
|
this.stats.totalExpired++;
|
|
38596
|
-
this.emit("
|
|
42042
|
+
this.emit("plg:ttl:record-expired", { resource: entry.resourceName, record });
|
|
38597
42043
|
} catch (error) {
|
|
38598
42044
|
console.error(`[TTLPlugin] Error processing expired entry:`, error);
|
|
38599
42045
|
this.stats.totalErrors++;
|
|
@@ -39064,15 +42510,15 @@ class VectorPlugin extends Plugin {
|
|
|
39064
42510
|
this._throttleState = /* @__PURE__ */ new Map();
|
|
39065
42511
|
}
|
|
39066
42512
|
async onInstall() {
|
|
39067
|
-
this.emit("installed", { plugin: "VectorPlugin" });
|
|
42513
|
+
this.emit("db:plugin:installed", { plugin: "VectorPlugin" });
|
|
39068
42514
|
this.validateVectorStorage();
|
|
39069
42515
|
this.installResourceMethods();
|
|
39070
42516
|
}
|
|
39071
42517
|
async onStart() {
|
|
39072
|
-
this.emit("started", { plugin: "VectorPlugin" });
|
|
42518
|
+
this.emit("db:plugin:started", { plugin: "VectorPlugin" });
|
|
39073
42519
|
}
|
|
39074
42520
|
async onStop() {
|
|
39075
|
-
this.emit("stopped", { plugin: "VectorPlugin" });
|
|
42521
|
+
this.emit("db:plugin:stopped", { plugin: "VectorPlugin" });
|
|
39076
42522
|
}
|
|
39077
42523
|
async onUninstall(options) {
|
|
39078
42524
|
for (const resource of Object.values(this.database.resources)) {
|
|
@@ -39083,7 +42529,7 @@ class VectorPlugin extends Plugin {
|
|
|
39083
42529
|
delete resource.findSimilar;
|
|
39084
42530
|
delete resource.distance;
|
|
39085
42531
|
}
|
|
39086
|
-
this.emit("uninstalled", { plugin: "VectorPlugin" });
|
|
42532
|
+
this.emit("db:plugin:uninstalled", { plugin: "VectorPlugin" });
|
|
39087
42533
|
}
|
|
39088
42534
|
/**
|
|
39089
42535
|
* Validate vector storage configuration for all resources
|
|
@@ -39112,10 +42558,10 @@ class VectorPlugin extends Plugin {
|
|
|
39112
42558
|
currentBehavior: resource.behavior || "default",
|
|
39113
42559
|
recommendation: "body-overflow"
|
|
39114
42560
|
};
|
|
39115
|
-
this.emit("vector:storage-warning", warning);
|
|
42561
|
+
this.emit("plg:vector:storage-warning", warning);
|
|
39116
42562
|
if (this.config.autoFixBehavior) {
|
|
39117
42563
|
resource.behavior = "body-overflow";
|
|
39118
|
-
this.emit("vector:behavior-fixed", {
|
|
42564
|
+
this.emit("plg:vector:behavior-fixed", {
|
|
39119
42565
|
resource: resource.name,
|
|
39120
42566
|
newBehavior: "body-overflow"
|
|
39121
42567
|
});
|
|
@@ -39147,7 +42593,7 @@ class VectorPlugin extends Plugin {
|
|
|
39147
42593
|
const partitionName = `byHas${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
|
|
39148
42594
|
const trackingFieldName = `_has${this.capitalize(vectorField.name.replace(/\./g, "_"))}`;
|
|
39149
42595
|
if (resource.config.partitions && resource.config.partitions[partitionName]) {
|
|
39150
|
-
this.emit("vector:partition-exists", {
|
|
42596
|
+
this.emit("plg:vector:partition-exists", {
|
|
39151
42597
|
resource: resource.name,
|
|
39152
42598
|
vectorField: vectorField.name,
|
|
39153
42599
|
partition: partitionName,
|
|
@@ -39170,7 +42616,7 @@ class VectorPlugin extends Plugin {
|
|
|
39170
42616
|
default: false
|
|
39171
42617
|
}, "VectorPlugin");
|
|
39172
42618
|
}
|
|
39173
|
-
this.emit("vector:partition-created", {
|
|
42619
|
+
this.emit("plg:vector:partition-created", {
|
|
39174
42620
|
resource: resource.name,
|
|
39175
42621
|
vectorField: vectorField.name,
|
|
39176
42622
|
partition: partitionName,
|
|
@@ -39245,7 +42691,7 @@ class VectorPlugin extends Plugin {
|
|
|
39245
42691
|
}
|
|
39246
42692
|
return updates;
|
|
39247
42693
|
});
|
|
39248
|
-
this.emit("vector:hooks-installed", {
|
|
42694
|
+
this.emit("plg:vector:hooks-installed", {
|
|
39249
42695
|
resource: resource.name,
|
|
39250
42696
|
vectorField,
|
|
39251
42697
|
trackingField,
|
|
@@ -39354,7 +42800,7 @@ class VectorPlugin extends Plugin {
|
|
|
39354
42800
|
const vectorField = this._findEmbeddingField(resource.schema.attributes);
|
|
39355
42801
|
this._vectorFieldCache.set(resource.name, vectorField);
|
|
39356
42802
|
if (vectorField && this.config.emitEvents) {
|
|
39357
|
-
this.emit("vector:field-detected", {
|
|
42803
|
+
this.emit("plg:vector:field-detected", {
|
|
39358
42804
|
resource: resource.name,
|
|
39359
42805
|
vectorField,
|
|
39360
42806
|
timestamp: Date.now()
|
|
@@ -40278,7 +43724,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40278
43724
|
async sendCommand(command) {
|
|
40279
43725
|
const commandName = command.constructor.name;
|
|
40280
43726
|
const input = command.input || {};
|
|
40281
|
-
this.emit("
|
|
43727
|
+
this.emit("cl:request", commandName, input);
|
|
40282
43728
|
let response;
|
|
40283
43729
|
try {
|
|
40284
43730
|
switch (commandName) {
|
|
@@ -40306,7 +43752,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40306
43752
|
default:
|
|
40307
43753
|
throw new Error(`Unsupported command: ${commandName}`);
|
|
40308
43754
|
}
|
|
40309
|
-
this.emit("
|
|
43755
|
+
this.emit("cl:response", commandName, response, input);
|
|
40310
43756
|
return response;
|
|
40311
43757
|
} catch (error) {
|
|
40312
43758
|
const mappedError = mapAwsError(error, {
|
|
@@ -40417,7 +43863,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40417
43863
|
contentLength,
|
|
40418
43864
|
ifMatch
|
|
40419
43865
|
});
|
|
40420
|
-
this.emit("
|
|
43866
|
+
this.emit("cl:PutObject", null, { key, metadata, contentType, body, contentEncoding, contentLength });
|
|
40421
43867
|
return response;
|
|
40422
43868
|
}
|
|
40423
43869
|
/**
|
|
@@ -40432,7 +43878,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40432
43878
|
decodedMetadata[k] = metadataDecode(v);
|
|
40433
43879
|
}
|
|
40434
43880
|
}
|
|
40435
|
-
this.emit("
|
|
43881
|
+
this.emit("cl:GetObject", null, { key });
|
|
40436
43882
|
return {
|
|
40437
43883
|
...response,
|
|
40438
43884
|
Metadata: decodedMetadata
|
|
@@ -40450,7 +43896,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40450
43896
|
decodedMetadata[k] = metadataDecode(v);
|
|
40451
43897
|
}
|
|
40452
43898
|
}
|
|
40453
|
-
this.emit("
|
|
43899
|
+
this.emit("cl:HeadObject", null, { key });
|
|
40454
43900
|
return {
|
|
40455
43901
|
...response,
|
|
40456
43902
|
Metadata: decodedMetadata
|
|
@@ -40475,7 +43921,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40475
43921
|
metadataDirective,
|
|
40476
43922
|
contentType
|
|
40477
43923
|
});
|
|
40478
|
-
this.emit("
|
|
43924
|
+
this.emit("cl:CopyObject", null, { from, to, metadata, metadataDirective });
|
|
40479
43925
|
return response;
|
|
40480
43926
|
}
|
|
40481
43927
|
/**
|
|
@@ -40491,7 +43937,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40491
43937
|
async deleteObject(key) {
|
|
40492
43938
|
const fullKey = this.keyPrefix ? path$1.join(this.keyPrefix, key) : key;
|
|
40493
43939
|
const response = await this.storage.delete(fullKey);
|
|
40494
|
-
this.emit("
|
|
43940
|
+
this.emit("cl:DeleteObject", null, { key });
|
|
40495
43941
|
return response;
|
|
40496
43942
|
}
|
|
40497
43943
|
/**
|
|
@@ -40524,7 +43970,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40524
43970
|
maxKeys,
|
|
40525
43971
|
continuationToken
|
|
40526
43972
|
});
|
|
40527
|
-
this.emit("
|
|
43973
|
+
this.emit("cl:ListObjects", null, { prefix, count: response.Contents.length });
|
|
40528
43974
|
return response;
|
|
40529
43975
|
}
|
|
40530
43976
|
/**
|
|
@@ -40564,7 +44010,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40564
44010
|
if (this.keyPrefix) {
|
|
40565
44011
|
keys = keys.map((x) => x.replace(this.keyPrefix, "")).map((x) => x.startsWith("/") ? x.replace("/", "") : x);
|
|
40566
44012
|
}
|
|
40567
|
-
this.emit("
|
|
44013
|
+
this.emit("cl:GetKeysPage", keys, params);
|
|
40568
44014
|
return keys;
|
|
40569
44015
|
}
|
|
40570
44016
|
/**
|
|
@@ -40581,7 +44027,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40581
44027
|
if (this.keyPrefix) {
|
|
40582
44028
|
keys = keys.map((x) => x.replace(this.keyPrefix, "")).map((x) => x.startsWith("/") ? x.replace("/", "") : x);
|
|
40583
44029
|
}
|
|
40584
|
-
this.emit("
|
|
44030
|
+
this.emit("cl:GetAllKeys", keys, { prefix });
|
|
40585
44031
|
return keys;
|
|
40586
44032
|
}
|
|
40587
44033
|
/**
|
|
@@ -40590,7 +44036,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40590
44036
|
async count({ prefix = "" } = {}) {
|
|
40591
44037
|
const keys = await this.getAllKeys({ prefix });
|
|
40592
44038
|
const count = keys.length;
|
|
40593
|
-
this.emit("
|
|
44039
|
+
this.emit("cl:Count", count, { prefix });
|
|
40594
44040
|
return count;
|
|
40595
44041
|
}
|
|
40596
44042
|
/**
|
|
@@ -40602,13 +44048,13 @@ class MemoryClient extends EventEmitter {
|
|
|
40602
44048
|
if (keys.length > 0) {
|
|
40603
44049
|
const result = await this.deleteObjects(keys);
|
|
40604
44050
|
totalDeleted = result.Deleted.length;
|
|
40605
|
-
this.emit("
|
|
44051
|
+
this.emit("cl:DeleteAll", {
|
|
40606
44052
|
prefix,
|
|
40607
44053
|
batch: totalDeleted,
|
|
40608
44054
|
total: totalDeleted
|
|
40609
44055
|
});
|
|
40610
44056
|
}
|
|
40611
|
-
this.emit("
|
|
44057
|
+
this.emit("cl:DeleteAllComplete", {
|
|
40612
44058
|
prefix,
|
|
40613
44059
|
totalDeleted
|
|
40614
44060
|
});
|
|
@@ -40621,11 +44067,11 @@ class MemoryClient extends EventEmitter {
|
|
|
40621
44067
|
if (offset === 0) return null;
|
|
40622
44068
|
const keys = await this.getAllKeys({ prefix });
|
|
40623
44069
|
if (offset >= keys.length) {
|
|
40624
|
-
this.emit("
|
|
44070
|
+
this.emit("cl:GetContinuationTokenAfterOffset", null, { prefix, offset });
|
|
40625
44071
|
return null;
|
|
40626
44072
|
}
|
|
40627
44073
|
const token = keys[offset];
|
|
40628
|
-
this.emit("
|
|
44074
|
+
this.emit("cl:GetContinuationTokenAfterOffset", token, { prefix, offset });
|
|
40629
44075
|
return token;
|
|
40630
44076
|
}
|
|
40631
44077
|
/**
|
|
@@ -40655,7 +44101,7 @@ class MemoryClient extends EventEmitter {
|
|
|
40655
44101
|
});
|
|
40656
44102
|
}
|
|
40657
44103
|
}
|
|
40658
|
-
this.emit("
|
|
44104
|
+
this.emit("cl:MoveAllObjects", { results, errors }, { prefixFrom, prefixTo });
|
|
40659
44105
|
if (errors.length > 0) {
|
|
40660
44106
|
const error = new Error("Some objects could not be moved");
|
|
40661
44107
|
error.context = {
|