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.
Files changed (37) hide show
  1. package/README.md +9 -9
  2. package/dist/s3db.cjs.js +3637 -191
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.es.js +3637 -191
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/clients/memory-client.class.js +16 -16
  8. package/src/clients/s3-client.class.js +17 -17
  9. package/src/concerns/error-classifier.js +204 -0
  10. package/src/database.class.js +9 -9
  11. package/src/plugins/api/index.js +1 -7
  12. package/src/plugins/api/routes/resource-routes.js +3 -3
  13. package/src/plugins/api/server.js +29 -9
  14. package/src/plugins/audit.plugin.js +2 -4
  15. package/src/plugins/backup.plugin.js +10 -12
  16. package/src/plugins/cache.plugin.js +4 -6
  17. package/src/plugins/concerns/plugin-dependencies.js +12 -0
  18. package/src/plugins/costs.plugin.js +0 -2
  19. package/src/plugins/eventual-consistency/index.js +1 -3
  20. package/src/plugins/fulltext.plugin.js +2 -4
  21. package/src/plugins/geo.plugin.js +3 -5
  22. package/src/plugins/importer/index.js +0 -2
  23. package/src/plugins/index.js +0 -1
  24. package/src/plugins/metrics.plugin.js +2 -4
  25. package/src/plugins/ml.plugin.js +1004 -42
  26. package/src/plugins/plugin.class.js +1 -3
  27. package/src/plugins/queue-consumer.plugin.js +1 -3
  28. package/src/plugins/relation.plugin.js +2 -4
  29. package/src/plugins/replicator.plugin.js +18 -20
  30. package/src/plugins/s3-queue.plugin.js +6 -8
  31. package/src/plugins/scheduler.plugin.js +9 -11
  32. package/src/plugins/state-machine.errors.js +9 -1
  33. package/src/plugins/state-machine.plugin.js +605 -20
  34. package/src/plugins/tfstate/index.js +0 -2
  35. package/src/plugins/ttl.plugin.js +40 -25
  36. package/src/plugins/vector.plugin.js +10 -12
  37. 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("backup_start", { id: backupId, type });
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("backup_complete", {
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("backup_error", { id: backupId, type, error: error.message });
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("restore_start", { id: backupId, options });
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("restore_complete", {
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("restore_error", { id: backupId, error: error.message });
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("backup_cancelled", { id: backupId });
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("cache_clear_error", {
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("cache_clear_error", {
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("cache_clear_error", {
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("@tensorflow/tfjs-node", "MLPlugin");
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("[MLPlugin] Purged all model data");
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
- const [ok, err, data] = await tryFn(() => resource.list());
13464
- if (!ok) {
13465
- throw new TrainingError(
13466
- `Failed to fetch training data: ${err.message}`,
13467
- { modelName, resource: modelConfig.resource, originalError: err.message }
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
- await this._saveModel(modelName);
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("modelTrained", {
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
- await storage.patch(`model_${modelName}`, {
13644
- modelName,
13645
- data: JSON.stringify(exportedModel),
13646
- savedAt: (/* @__PURE__ */ new Date()).toISOString()
13647
- });
13648
- if (this.config.verbose) {
13649
- console.log(`[MLPlugin] Saved model "${modelName}" to plugin storage`);
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 [ok, err, record] = await tryFn(() => storage.get(`model_${modelName}`));
13663
- if (!ok || !record) {
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
- * Delete model from plugin storage
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
- await storage.delete(`model_${modelName}`);
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 storage`);
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("command.request", command.constructor.name, command.input);
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("command.response", command.constructor.name, response, command.input);
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("putObject", err || response, { key, metadata, contentType, body, contentEncoding, contentLength });
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("getObject", err || response, { key });
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("headObject", err || response, { key });
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("copyObject", err || response, { from, to, metadataDirective });
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("deleteObject", err || response, { key });
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("deleteObjects", report, keys);
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("deleteAll", {
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("deleteAllComplete", {
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("listObjects", response, options);
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("count", count, { prefix });
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("getAllKeys", keys, { prefix });
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("getContinuationTokenAfterOffset", continuationToken || null, params);
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("getKeysPage", [], params);
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("getKeysPage", keys, params);
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("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
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.emit("insert", finalResult);
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.emit("insert", finalResult);
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.emit("get", data);
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.emit("update", {
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.emit("update", {
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.emit("update", {
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.emit("update", {
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.emit("delete", {
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.emit("count", count);
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.emit("insertMany", objects.length);
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.emit("deleteMany", ids.length);
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.emit("deleteAll", {
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.emit("deleteAllData", {
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.emit("listIds", ids.length);
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.emit("list", { count: results.length, errors: 0 });
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.emit("list", { partition, partitionValues, count: 0, errors: 0 });
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.emit("list", { partition, partitionValues, count: results.length, errors: 0 });
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.emit("list", { count: results.length, errors: 0 });
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.emit("list", { partition, partitionValues, count: 0, errors: 1 });
25099
+ this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
22271
25100
  return [];
22272
25101
  }
22273
- this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
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.emit("getMany", ids.length);
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.emit("page", result2);
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.emit("setContent", { id, contentType, contentLength: buffer.length });
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.emit("content", id, buffer.length, contentType);
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.emit("deleteContent", id);
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.emit("getFromPartition", data);
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.0.0" : "latest");
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("resourceDefinitionsChanged", {
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.uninstalled", { name: pluginName, 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("metadataUploaded", metadata);
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("metadataHealed", { healingLog, metadata });
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("s3db.resourceUpdated", name);
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("s3db.resourceCreated", name);
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("hookError", { event, error, context });
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("replicator_log_error", {
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("replicator_error", {
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("replicator_error", {
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("replicator_error", {
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.log.failed", { error: "replicator log resource not found", item });
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.log.failed", { error: err, item });
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.updateLog.failed", { error: err.message, logId, updates });
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.sync.resource", { resourceName, replicatorId });
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.sync.completed", { replicatorId, stats: this.stats });
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("replicator_stop_error", {
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("replicator_plugin_stop_error", {
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.started", { concurrency, workerId: this.workerId });
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.stopped", { workerId: this.workerId });
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.completed", {
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.retry", {
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.dead", {
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("job_start", { jobName, executionId, startTime });
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("job_complete", {
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("job_enabled", { jobName });
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("job_disabled", { jobName });
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("job_added", { jobName });
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("job_removed", { jobName });
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
- this.emit("initialized", { machines: Array.from(this.machines.keys()) });
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 [ok, error] = await tryFn(
27746
- () => action(context, event, { database: this.database, machineId, entityId })
27747
- );
27748
- if (!ok) {
27749
- if (this.config.verbose) {
27750
- console.error(`[StateMachinePlugin] Action '${actionName}' failed:`, error.message);
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("entity_initialized", { machineId, entityId, initialState });
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: 3600,
41631
+ threshold: ONE_HOUR_SEC,
38186
41632
  // TTL < 1 hour
38187
- interval: 1e4,
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: 86400,
41641
+ threshold: ONE_DAY_SEC,
38196
41642
  // TTL < 24 hours
38197
- interval: 6e5,
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: 2592e3,
41651
+ threshold: THIRTY_DAYS_SEC,
38206
41652
  // TTL < 30 days
38207
- interval: 36e5,
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: 864e5,
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) / 864e5 + 1) / 7);
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 * 6e4);
41696
+ checkDate = new Date(now.getTime() - i * ONE_MINUTE_MS);
38251
41697
  break;
38252
41698
  case "hour":
38253
- checkDate = new Date(now.getTime() - i * 36e5);
41699
+ checkDate = new Date(now.getTime() - i * ONE_HOUR_MS);
38254
41700
  break;
38255
41701
  case "day":
38256
- checkDate = new Date(now.getTime() - i * 864e5);
41702
+ checkDate = new Date(now.getTime() - i * ONE_DAY_MS);
38257
41703
  break;
38258
41704
  case "week":
38259
- checkDate = new Date(now.getTime() - i * 6048e5);
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 * 1e3) : new Date(baseTimestamp);
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("scanCompleted", {
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("cleanupError", { granularity, error });
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("recordExpired", { resource: entry.resourceName, record });
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("command.request", commandName, input);
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("command.response", commandName, response, input);
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("putObject", null, { key, metadata, contentType, body, contentEncoding, contentLength });
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("getObject", null, { key });
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("headObject", null, { key });
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("copyObject", null, { from, to, metadata, metadataDirective });
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("deleteObject", null, { key });
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("listObjects", null, { prefix, count: response.Contents.length });
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("getKeysPage", keys, params);
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("getAllKeys", keys, { prefix });
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("count", count, { prefix });
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("deleteAll", {
44051
+ this.emit("cl:DeleteAll", {
40606
44052
  prefix,
40607
44053
  batch: totalDeleted,
40608
44054
  total: totalDeleted
40609
44055
  });
40610
44056
  }
40611
- this.emit("deleteAllComplete", {
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("getContinuationTokenAfterOffset", null, { prefix, offset });
44070
+ this.emit("cl:GetContinuationTokenAfterOffset", null, { prefix, offset });
40625
44071
  return null;
40626
44072
  }
40627
44073
  const token = keys[offset];
40628
- this.emit("getContinuationTokenAfterOffset", token, { prefix, offset });
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("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
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 = {