s3db.js 13.4.0 → 13.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/s3db.cjs.js +12167 -11177
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +12168 -11178
- package/dist/s3db.es.js.map +1 -1
- package/package.json +3 -3
- package/src/database.class.js +2 -2
- package/src/plugins/api/auth/basic-auth.js +17 -9
- package/src/plugins/api/index.js +23 -19
- package/src/plugins/api/routes/auth-routes.js +100 -79
- package/src/plugins/api/routes/resource-routes.js +3 -2
- package/src/plugins/api/server.js +176 -5
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/openapi-generator.js +52 -6
- package/src/plugins/concerns/plugin-dependencies.js +1 -1
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- package/src/plugins/ml/base-model.class.js +33 -9
- package/src/plugins/ml.plugin.js +474 -13
- package/src/plugins/state-machine.plugin.js +57 -2
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { createResourceRoutes, createRelationalRoutes } from './routes/resource-routes.js';
|
|
8
|
+
import { createAuthRoutes } from './routes/auth-routes.js';
|
|
9
|
+
import { mountCustomRoutes } from './utils/custom-routes.js';
|
|
8
10
|
import { errorHandler } from './utils/error-handler.js';
|
|
9
11
|
import * as formatter from './utils/response-formatter.js';
|
|
10
12
|
import { generateOpenAPISpec } from './utils/openapi-generator.js';
|
|
13
|
+
import { jwtAuth } from './auth/jwt-auth.js';
|
|
14
|
+
import { basicAuth } from './auth/basic-auth.js';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* API Server class
|
|
@@ -29,6 +33,7 @@ export class ApiServer {
|
|
|
29
33
|
host: options.host || '0.0.0.0',
|
|
30
34
|
database: options.database,
|
|
31
35
|
resources: options.resources || {},
|
|
36
|
+
routes: options.routes || {}, // Plugin-level custom routes
|
|
32
37
|
middlewares: options.middlewares || [],
|
|
33
38
|
verbose: options.verbose || false,
|
|
34
39
|
auth: options.auth || {},
|
|
@@ -36,6 +41,7 @@ export class ApiServer {
|
|
|
36
41
|
docsUI: options.docsUI || 'redoc', // 'swagger' or 'redoc'
|
|
37
42
|
maxBodySize: options.maxBodySize || 10 * 1024 * 1024, // 10MB default
|
|
38
43
|
rootHandler: options.rootHandler, // Custom handler for root path, if not provided redirects to /docs
|
|
44
|
+
versionPrefix: options.versionPrefix, // Global version prefix config
|
|
39
45
|
apiInfo: {
|
|
40
46
|
title: options.apiTitle || 's3db.js API',
|
|
41
47
|
version: options.apiVersion || '1.0.0',
|
|
@@ -198,11 +204,19 @@ export class ApiServer {
|
|
|
198
204
|
// Setup resource routes
|
|
199
205
|
this._setupResourceRoutes();
|
|
200
206
|
|
|
207
|
+
// Setup authentication routes if driver is configured
|
|
208
|
+
if (this.options.auth.driver) {
|
|
209
|
+
this._setupAuthRoutes();
|
|
210
|
+
}
|
|
211
|
+
|
|
201
212
|
// Setup relational routes if RelationPlugin is active
|
|
202
213
|
if (this.relationsPlugin) {
|
|
203
214
|
this._setupRelationalRoutes();
|
|
204
215
|
}
|
|
205
216
|
|
|
217
|
+
// Setup plugin-level custom routes
|
|
218
|
+
this._setupPluginRoutes();
|
|
219
|
+
|
|
206
220
|
// Global error handler
|
|
207
221
|
this.app.onError((err, c) => {
|
|
208
222
|
return errorHandler(err, c);
|
|
@@ -247,22 +261,154 @@ export class ApiServer {
|
|
|
247
261
|
// Determine version
|
|
248
262
|
const version = resource.config?.currentVersion || resource.version || 'v1';
|
|
249
263
|
|
|
264
|
+
// Determine version prefix (resource-level overrides global)
|
|
265
|
+
// Priority: resource.versionPrefix > global versionPrefix > false (default - no prefix)
|
|
266
|
+
let versionPrefixConfig = config.versionPrefix !== undefined
|
|
267
|
+
? config.versionPrefix
|
|
268
|
+
: this.options.versionPrefix !== undefined
|
|
269
|
+
? this.options.versionPrefix
|
|
270
|
+
: false;
|
|
271
|
+
|
|
272
|
+
// Calculate the actual prefix to use
|
|
273
|
+
let prefix = '';
|
|
274
|
+
if (versionPrefixConfig === true) {
|
|
275
|
+
// true: use resource version
|
|
276
|
+
prefix = version;
|
|
277
|
+
} else if (versionPrefixConfig === false) {
|
|
278
|
+
// false: no prefix
|
|
279
|
+
prefix = '';
|
|
280
|
+
} else if (typeof versionPrefixConfig === 'string') {
|
|
281
|
+
// string: custom prefix
|
|
282
|
+
prefix = versionPrefixConfig;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Prepare custom middleware
|
|
286
|
+
const middlewares = [...(config.customMiddleware || [])];
|
|
287
|
+
|
|
288
|
+
// Add authentication middleware if required
|
|
289
|
+
if (config.auth && this.options.auth.driver) {
|
|
290
|
+
const authMiddleware = this._createAuthMiddleware();
|
|
291
|
+
if (authMiddleware) {
|
|
292
|
+
middlewares.unshift(authMiddleware); // Add at beginning
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
250
296
|
// Create resource routes
|
|
251
297
|
const resourceApp = createResourceRoutes(resource, version, {
|
|
252
298
|
methods: config.methods,
|
|
253
|
-
customMiddleware:
|
|
254
|
-
enableValidation: config.validation !== false
|
|
299
|
+
customMiddleware: middlewares,
|
|
300
|
+
enableValidation: config.validation !== false,
|
|
301
|
+
versionPrefix: prefix
|
|
255
302
|
}, this.Hono);
|
|
256
303
|
|
|
257
|
-
// Mount resource routes
|
|
258
|
-
|
|
304
|
+
// Mount resource routes (with or without prefix)
|
|
305
|
+
const mountPath = prefix ? `/${prefix}/${name}` : `/${name}`;
|
|
306
|
+
this.app.route(mountPath, resourceApp);
|
|
259
307
|
|
|
260
308
|
if (this.options.verbose) {
|
|
261
|
-
console.log(`[API Plugin] Mounted routes for resource '${name}' at
|
|
309
|
+
console.log(`[API Plugin] Mounted routes for resource '${name}' at ${mountPath}`);
|
|
262
310
|
}
|
|
311
|
+
|
|
312
|
+
// Mount custom routes for this resource (if defined)
|
|
313
|
+
if (config.routes) {
|
|
314
|
+
const routeContext = {
|
|
315
|
+
resource,
|
|
316
|
+
database,
|
|
317
|
+
resourceName: name,
|
|
318
|
+
version
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Mount on the resourceApp (nested under resource path)
|
|
322
|
+
mountCustomRoutes(resourceApp, config.routes, routeContext, this.options.verbose);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Setup authentication routes (when auth driver is configured)
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
_setupAuthRoutes() {
|
|
332
|
+
const { database, auth } = this.options;
|
|
333
|
+
const { driver, resource: resourceName, usernameField, passwordField, config } = auth;
|
|
334
|
+
|
|
335
|
+
// Get auth resource from database
|
|
336
|
+
const authResource = database.resources[resourceName];
|
|
337
|
+
if (!authResource) {
|
|
338
|
+
console.error(`[API Plugin] Auth resource '${resourceName}' not found. Skipping auth routes.`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Prepare auth config for routes
|
|
343
|
+
const authConfig = {
|
|
344
|
+
driver,
|
|
345
|
+
usernameField,
|
|
346
|
+
passwordField,
|
|
347
|
+
jwtSecret: config.jwtSecret || config.secret,
|
|
348
|
+
jwtExpiresIn: config.jwtExpiresIn || config.expiresIn || '7d',
|
|
349
|
+
passphrase: config.passphrase || 'secret',
|
|
350
|
+
allowRegistration: config.allowRegistration !== false
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// Create auth routes
|
|
354
|
+
const authApp = createAuthRoutes(authResource, authConfig);
|
|
355
|
+
|
|
356
|
+
// Mount auth routes at /auth
|
|
357
|
+
this.app.route('/auth', authApp);
|
|
358
|
+
|
|
359
|
+
if (this.options.verbose) {
|
|
360
|
+
console.log(`[API Plugin] Mounted auth routes (driver: ${driver}) at /auth`);
|
|
263
361
|
}
|
|
264
362
|
}
|
|
265
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Create authentication middleware based on driver
|
|
366
|
+
* @private
|
|
367
|
+
* @returns {Function|null} Auth middleware function
|
|
368
|
+
*/
|
|
369
|
+
_createAuthMiddleware() {
|
|
370
|
+
const { database, auth } = this.options;
|
|
371
|
+
const { driver, resource: resourceName, usernameField, passwordField, config } = auth;
|
|
372
|
+
|
|
373
|
+
if (!driver) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const authResource = database.resources[resourceName];
|
|
378
|
+
if (!authResource) {
|
|
379
|
+
console.error(`[API Plugin] Auth resource '${resourceName}' not found for middleware`);
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (driver === 'jwt') {
|
|
384
|
+
const jwtSecret = config.jwtSecret || config.secret;
|
|
385
|
+
if (!jwtSecret) {
|
|
386
|
+
console.error('[API Plugin] JWT driver requires jwtSecret in config');
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return jwtAuth({
|
|
391
|
+
secret: jwtSecret,
|
|
392
|
+
authResource,
|
|
393
|
+
usernameField,
|
|
394
|
+
passwordField
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (driver === 'basic') {
|
|
399
|
+
return basicAuth({
|
|
400
|
+
realm: config.realm || 'API Access',
|
|
401
|
+
authResource,
|
|
402
|
+
usernameField,
|
|
403
|
+
passwordField,
|
|
404
|
+
passphrase: config.passphrase || 'secret'
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
console.error(`[API Plugin] Unknown auth driver: ${driver}`);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
266
412
|
/**
|
|
267
413
|
* Setup relational routes (when RelationPlugin is active)
|
|
268
414
|
* @private
|
|
@@ -332,6 +478,31 @@ export class ApiServer {
|
|
|
332
478
|
}
|
|
333
479
|
}
|
|
334
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Setup plugin-level custom routes
|
|
483
|
+
* @private
|
|
484
|
+
*/
|
|
485
|
+
_setupPluginRoutes() {
|
|
486
|
+
const { routes, database } = this.options;
|
|
487
|
+
|
|
488
|
+
if (!routes || Object.keys(routes).length === 0) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Plugin-level routes context
|
|
493
|
+
const context = {
|
|
494
|
+
database,
|
|
495
|
+
plugins: database?.plugins || {}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// Mount plugin routes directly on main app (not nested)
|
|
499
|
+
mountCustomRoutes(this.app, routes, context, this.options.verbose);
|
|
500
|
+
|
|
501
|
+
if (this.options.verbose) {
|
|
502
|
+
console.log(`[API Plugin] Mounted ${Object.keys(routes).length} plugin-level custom routes`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
335
506
|
/**
|
|
336
507
|
* Start the server
|
|
337
508
|
* @returns {Promise<void>}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Routes Utilities
|
|
3
|
+
*
|
|
4
|
+
* Parse and mount custom routes defined in resources or plugins
|
|
5
|
+
* Inspired by moleculer-js route syntax
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { asyncHandler } from './error-handler.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse route definition from key
|
|
12
|
+
* @param {string} key - Route key (e.g., 'GET /users', 'POST /custom/:id/action')
|
|
13
|
+
* @returns {Object} { method, path }
|
|
14
|
+
*/
|
|
15
|
+
export function parseRouteKey(key) {
|
|
16
|
+
const match = key.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(.+)$/i);
|
|
17
|
+
|
|
18
|
+
if (!match) {
|
|
19
|
+
throw new Error(`Invalid route key format: "${key}". Expected format: "METHOD /path"`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
method: match[1].toUpperCase(),
|
|
24
|
+
path: match[2]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mount custom routes on Hono app
|
|
30
|
+
* @param {Object} app - Hono app instance
|
|
31
|
+
* @param {Object} routes - Routes object { 'METHOD /path': handler }
|
|
32
|
+
* @param {Object} context - Context to pass to handlers (resource, database, etc.)
|
|
33
|
+
* @param {boolean} verbose - Enable verbose logging
|
|
34
|
+
*/
|
|
35
|
+
export function mountCustomRoutes(app, routes, context = {}, verbose = false) {
|
|
36
|
+
if (!routes || typeof routes !== 'object') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [key, handler] of Object.entries(routes)) {
|
|
41
|
+
try {
|
|
42
|
+
const { method, path } = parseRouteKey(key);
|
|
43
|
+
|
|
44
|
+
// Wrap handler with async error handler and context
|
|
45
|
+
const wrappedHandler = asyncHandler(async (c) => {
|
|
46
|
+
// Inject context into Hono context
|
|
47
|
+
c.set('customRouteContext', context);
|
|
48
|
+
|
|
49
|
+
// Call user handler with Hono context
|
|
50
|
+
return await handler(c);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Mount route
|
|
54
|
+
app.on(method, path, wrappedHandler);
|
|
55
|
+
|
|
56
|
+
if (verbose) {
|
|
57
|
+
console.log(`[Custom Routes] Mounted ${method} ${path}`);
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(`[Custom Routes] Error mounting route "${key}":`, err.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate custom routes object
|
|
67
|
+
* @param {Object} routes - Routes to validate
|
|
68
|
+
* @returns {Array} Array of validation errors
|
|
69
|
+
*/
|
|
70
|
+
export function validateCustomRoutes(routes) {
|
|
71
|
+
const errors = [];
|
|
72
|
+
|
|
73
|
+
if (!routes || typeof routes !== 'object') {
|
|
74
|
+
return errors;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const [key, handler] of Object.entries(routes)) {
|
|
78
|
+
// Validate key format
|
|
79
|
+
try {
|
|
80
|
+
parseRouteKey(key);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
errors.push({ key, error: err.message });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate handler is a function
|
|
87
|
+
if (typeof handler !== 'function') {
|
|
88
|
+
errors.push({
|
|
89
|
+
key,
|
|
90
|
+
error: `Handler must be a function, got ${typeof handler}`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return errors;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default {
|
|
99
|
+
parseRouteKey,
|
|
100
|
+
mountCustomRoutes,
|
|
101
|
+
validateCustomRoutes
|
|
102
|
+
};
|
|
@@ -185,7 +185,20 @@ function generateResourceSchema(resource) {
|
|
|
185
185
|
*/
|
|
186
186
|
function generateResourcePaths(resource, version, config = {}) {
|
|
187
187
|
const resourceName = resource.name;
|
|
188
|
-
|
|
188
|
+
|
|
189
|
+
// Determine version prefix (same logic as server.js)
|
|
190
|
+
let versionPrefixConfig = config.versionPrefix !== undefined ? config.versionPrefix : false;
|
|
191
|
+
|
|
192
|
+
let prefix = '';
|
|
193
|
+
if (versionPrefixConfig === true) {
|
|
194
|
+
prefix = version;
|
|
195
|
+
} else if (versionPrefixConfig === false) {
|
|
196
|
+
prefix = '';
|
|
197
|
+
} else if (typeof versionPrefixConfig === 'string') {
|
|
198
|
+
prefix = versionPrefixConfig;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const basePath = prefix ? `/${prefix}/${resourceName}` : `/${resourceName}`;
|
|
189
202
|
const schema = generateResourceSchema(resource);
|
|
190
203
|
const methods = config.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
191
204
|
const authMethods = config.auth || [];
|
|
@@ -820,11 +833,14 @@ The response includes pagination metadata in the \`pagination\` object with tota
|
|
|
820
833
|
* @param {Object} relationConfig - Relation configuration
|
|
821
834
|
* @param {string} version - Resource version
|
|
822
835
|
* @param {Object} relatedSchema - OpenAPI schema for related resource
|
|
836
|
+
* @param {string} versionPrefix - Version prefix to use (empty string for no prefix)
|
|
823
837
|
* @returns {Object} OpenAPI paths for relation
|
|
824
838
|
*/
|
|
825
|
-
function generateRelationalPaths(resource, relationName, relationConfig, version, relatedSchema) {
|
|
839
|
+
function generateRelationalPaths(resource, relationName, relationConfig, version, relatedSchema, versionPrefix = '') {
|
|
826
840
|
const resourceName = resource.name;
|
|
827
|
-
const basePath =
|
|
841
|
+
const basePath = versionPrefix
|
|
842
|
+
? `/${versionPrefix}/${resourceName}/{id}/${relationName}`
|
|
843
|
+
: `/${resourceName}/{id}/${relationName}`;
|
|
828
844
|
const relatedResourceName = relationConfig.resource;
|
|
829
845
|
const isToMany = relationConfig.type === 'hasMany' || relationConfig.type === 'belongsToMany';
|
|
830
846
|
|
|
@@ -950,7 +966,24 @@ export function generateOpenAPISpec(database, config = {}) {
|
|
|
950
966
|
? resourceDescription.resource
|
|
951
967
|
: resourceDescription || 'No description';
|
|
952
968
|
|
|
953
|
-
|
|
969
|
+
// Check version prefix for this resource (same logic as server.js)
|
|
970
|
+
const config = resourceConfigs[name] || {};
|
|
971
|
+
let versionPrefixConfig = config.versionPrefix !== undefined
|
|
972
|
+
? config.versionPrefix
|
|
973
|
+
: false; // Default to false (no prefix)
|
|
974
|
+
|
|
975
|
+
let prefix = '';
|
|
976
|
+
if (versionPrefixConfig === true) {
|
|
977
|
+
prefix = version;
|
|
978
|
+
} else if (versionPrefixConfig === false) {
|
|
979
|
+
prefix = '';
|
|
980
|
+
} else if (typeof versionPrefixConfig === 'string') {
|
|
981
|
+
prefix = versionPrefixConfig;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const basePath = prefix ? `/${prefix}/${name}` : `/${name}`;
|
|
985
|
+
|
|
986
|
+
resourcesTableRows.push(`| ${name} | ${descText} | \`${basePath}\` |`);
|
|
954
987
|
}
|
|
955
988
|
|
|
956
989
|
// Build enhanced description with resources table
|
|
@@ -1084,6 +1117,18 @@ For detailed information about each endpoint, see the sections below.`;
|
|
|
1084
1117
|
// Determine version
|
|
1085
1118
|
const version = resource.config?.currentVersion || resource.version || 'v1';
|
|
1086
1119
|
|
|
1120
|
+
// Determine version prefix (same logic as server.js)
|
|
1121
|
+
let versionPrefixConfig = config.versionPrefix !== undefined ? config.versionPrefix : false;
|
|
1122
|
+
|
|
1123
|
+
let prefix = '';
|
|
1124
|
+
if (versionPrefixConfig === true) {
|
|
1125
|
+
prefix = version;
|
|
1126
|
+
} else if (versionPrefixConfig === false) {
|
|
1127
|
+
prefix = '';
|
|
1128
|
+
} else if (typeof versionPrefixConfig === 'string') {
|
|
1129
|
+
prefix = versionPrefixConfig;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1087
1132
|
// Generate paths
|
|
1088
1133
|
const paths = generateResourcePaths(resource, version, config);
|
|
1089
1134
|
|
|
@@ -1128,13 +1173,14 @@ For detailed information about each endpoint, see the sections below.`;
|
|
|
1128
1173
|
|
|
1129
1174
|
const relatedSchema = generateResourceSchema(relatedResource);
|
|
1130
1175
|
|
|
1131
|
-
// Generate relational paths
|
|
1176
|
+
// Generate relational paths (using the same prefix calculated above)
|
|
1132
1177
|
const relationalPaths = generateRelationalPaths(
|
|
1133
1178
|
resource,
|
|
1134
1179
|
relationName,
|
|
1135
1180
|
relationConfig,
|
|
1136
1181
|
version,
|
|
1137
|
-
relatedSchema
|
|
1182
|
+
relatedSchema,
|
|
1183
|
+
prefix
|
|
1138
1184
|
);
|
|
1139
1185
|
|
|
1140
1186
|
// Merge relational paths
|
|
@@ -170,7 +170,7 @@ async function tryLoadPackage(packageName) {
|
|
|
170
170
|
let version = null;
|
|
171
171
|
try {
|
|
172
172
|
const pkgJson = await import(`${packageName}/package.json`, {
|
|
173
|
-
|
|
173
|
+
with: { type: 'json' }
|
|
174
174
|
});
|
|
175
175
|
version = pkgJson.default?.version || pkgJson.version || null;
|
|
176
176
|
} catch (e) {
|
|
@@ -140,7 +140,7 @@ export async function runConsolidation(transactionResource, consolidateRecordFn,
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
if (emitFn) {
|
|
143
|
-
emitFn('eventual-consistency
|
|
143
|
+
emitFn('plg:eventual-consistency:consolidated', {
|
|
144
144
|
resource: config.resource,
|
|
145
145
|
field: config.field,
|
|
146
146
|
recordCount: uniqueIds.length,
|
|
@@ -157,7 +157,7 @@ export async function runConsolidation(transactionResource, consolidateRecordFn,
|
|
|
157
157
|
error
|
|
158
158
|
);
|
|
159
159
|
if (emitFn) {
|
|
160
|
-
emitFn('eventual-consistency
|
|
160
|
+
emitFn('plg:eventual-consistency:consolidation-error', error);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -103,7 +103,7 @@ export async function runGarbageCollection(transactionResource, storage, config,
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
if (emitFn) {
|
|
106
|
-
emitFn('eventual-consistency
|
|
106
|
+
emitFn('plg:eventual-consistency:gc-completed', {
|
|
107
107
|
resource: config.resource,
|
|
108
108
|
field: config.field,
|
|
109
109
|
deletedCount: results.length,
|
|
@@ -115,7 +115,7 @@ export async function runGarbageCollection(transactionResource, storage, config,
|
|
|
115
115
|
console.warn(`[EventualConsistency] GC error:`, error.message);
|
|
116
116
|
}
|
|
117
117
|
if (emitFn) {
|
|
118
|
-
emitFn('eventual-consistency
|
|
118
|
+
emitFn('plg:eventual-consistency:gc-error', error);
|
|
119
119
|
}
|
|
120
120
|
} finally {
|
|
121
121
|
// Always release GC lock
|
|
@@ -255,7 +255,7 @@ export async function onStart(fieldHandlers, config, runConsolidationFn, runGCFn
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
if (emitFn) {
|
|
258
|
-
emitFn('eventual-consistency
|
|
258
|
+
emitFn('plg:eventual-consistency:started', {
|
|
259
259
|
resource: resourceName,
|
|
260
260
|
field: fieldName,
|
|
261
261
|
cohort: config.cohort
|
|
@@ -295,7 +295,7 @@ export async function onStop(fieldHandlers, emitFn) {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
if (emitFn) {
|
|
298
|
-
emitFn('eventual-consistency
|
|
298
|
+
emitFn('plg:eventual-consistency:stopped', {
|
|
299
299
|
resource: resourceName,
|
|
300
300
|
field: fieldName
|
|
301
301
|
});
|
|
@@ -51,22 +51,36 @@ export class BaseModel {
|
|
|
51
51
|
errors: 0
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
this.
|
|
54
|
+
// TensorFlow will be loaded lazily on first use
|
|
55
|
+
this.tf = null;
|
|
56
|
+
this._tfValidated = false;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
|
-
* Validate TensorFlow.js
|
|
60
|
+
* Validate and load TensorFlow.js (lazy loading)
|
|
60
61
|
* @private
|
|
61
62
|
*/
|
|
62
|
-
_validateTensorFlow() {
|
|
63
|
+
async _validateTensorFlow() {
|
|
64
|
+
if (this._tfValidated) {
|
|
65
|
+
return; // Already validated and loaded
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
try {
|
|
69
|
+
// Try CommonJS require first (works in most environments)
|
|
64
70
|
this.tf = require('@tensorflow/tfjs-node');
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
this._tfValidated = true;
|
|
72
|
+
} catch (requireError) {
|
|
73
|
+
// If require fails (e.g., Jest VM modules), try dynamic import
|
|
74
|
+
try {
|
|
75
|
+
const tfModule = await import('@tensorflow/tfjs-node');
|
|
76
|
+
this.tf = tfModule.default || tfModule;
|
|
77
|
+
this._tfValidated = true;
|
|
78
|
+
} catch (importError) {
|
|
79
|
+
throw new TensorFlowDependencyError(
|
|
80
|
+
'TensorFlow.js is not installed. Run: pnpm add @tensorflow/tfjs-node',
|
|
81
|
+
{ originalError: importError.message }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
70
84
|
}
|
|
71
85
|
}
|
|
72
86
|
|
|
@@ -85,6 +99,11 @@ export class BaseModel {
|
|
|
85
99
|
* @returns {Object} Training results
|
|
86
100
|
*/
|
|
87
101
|
async train(data) {
|
|
102
|
+
// Validate TensorFlow on first use (lazy loading)
|
|
103
|
+
if (!this._tfValidated) {
|
|
104
|
+
await this._validateTensorFlow();
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
try {
|
|
89
108
|
if (!data || data.length === 0) {
|
|
90
109
|
throw new InsufficientDataError('No training data provided', {
|
|
@@ -171,6 +190,11 @@ export class BaseModel {
|
|
|
171
190
|
* @returns {Object} Prediction result
|
|
172
191
|
*/
|
|
173
192
|
async predict(input) {
|
|
193
|
+
// Validate TensorFlow on first use (lazy loading)
|
|
194
|
+
if (!this._tfValidated) {
|
|
195
|
+
await this._validateTensorFlow();
|
|
196
|
+
}
|
|
197
|
+
|
|
174
198
|
if (!this.isTrained) {
|
|
175
199
|
throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
|
|
176
200
|
model: this.config.name
|