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