s3db.js 12.1.0 → 12.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -196
- package/dist/s3db.cjs.js +1286 -226
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1284 -226
- package/dist/s3db.es.js.map +1 -1
- package/package.json +6 -1
- package/src/cli/index.js +954 -43
- package/src/cli/migration-manager.js +270 -0
- package/src/concerns/calculator.js +0 -4
- package/src/concerns/metadata-encoding.js +1 -21
- package/src/concerns/plugin-storage.js +17 -4
- package/src/concerns/typescript-generator.d.ts +171 -0
- package/src/concerns/typescript-generator.js +275 -0
- package/src/database.class.js +171 -28
- package/src/index.js +15 -9
- package/src/plugins/api/index.js +0 -1
- package/src/plugins/api/routes/resource-routes.js +86 -1
- package/src/plugins/api/server.js +79 -3
- package/src/plugins/api/utils/openapi-generator.js +195 -5
- package/src/plugins/backup/multi-backup-driver.class.js +0 -1
- package/src/plugins/backup.plugin.js +7 -14
- package/src/plugins/concerns/plugin-dependencies.js +73 -19
- package/src/plugins/eventual-consistency/analytics.js +0 -2
- package/src/plugins/eventual-consistency/consolidation.js +2 -13
- package/src/plugins/eventual-consistency/index.js +0 -1
- package/src/plugins/eventual-consistency/install.js +1 -1
- package/src/plugins/geo.plugin.js +5 -6
- package/src/plugins/importer/index.js +1 -1
- package/src/plugins/relation.plugin.js +11 -11
- package/src/plugins/replicator.plugin.js +12 -21
- package/src/plugins/s3-queue.plugin.js +4 -4
- package/src/plugins/scheduler.plugin.js +10 -12
- package/src/plugins/state-machine.plugin.js +8 -12
- package/src/plugins/tfstate/README.md +1 -1
- package/src/plugins/tfstate/errors.js +3 -3
- package/src/plugins/tfstate/index.js +41 -67
- package/src/plugins/ttl.plugin.js +3 -3
- package/src/resource.class.js +263 -61
- package/src/schema.class.js +0 -2
- package/src/testing/factory.class.js +286 -0
- package/src/testing/index.js +15 -0
- package/src/testing/seeder.class.js +183 -0
|
@@ -16,81 +16,97 @@
|
|
|
16
16
|
export const PLUGIN_DEPENDENCIES = {
|
|
17
17
|
'postgresql-replicator': {
|
|
18
18
|
name: 'PostgreSQL Replicator',
|
|
19
|
+
docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md',
|
|
19
20
|
dependencies: {
|
|
20
21
|
'pg': {
|
|
21
22
|
version: '^8.0.0',
|
|
22
23
|
description: 'PostgreSQL client for Node.js',
|
|
23
|
-
installCommand: 'pnpm add pg'
|
|
24
|
+
installCommand: 'pnpm add pg',
|
|
25
|
+
npmUrl: 'https://www.npmjs.com/package/pg'
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
},
|
|
27
29
|
'bigquery-replicator': {
|
|
28
30
|
name: 'BigQuery Replicator',
|
|
31
|
+
docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md',
|
|
29
32
|
dependencies: {
|
|
30
33
|
'@google-cloud/bigquery': {
|
|
31
34
|
version: '^7.0.0',
|
|
32
35
|
description: 'Google Cloud BigQuery SDK',
|
|
33
|
-
installCommand: 'pnpm add @google-cloud/bigquery'
|
|
36
|
+
installCommand: 'pnpm add @google-cloud/bigquery',
|
|
37
|
+
npmUrl: 'https://www.npmjs.com/package/@google-cloud/bigquery'
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
},
|
|
37
41
|
'sqs-replicator': {
|
|
38
42
|
name: 'SQS Replicator',
|
|
43
|
+
docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md',
|
|
39
44
|
dependencies: {
|
|
40
45
|
'@aws-sdk/client-sqs': {
|
|
41
46
|
version: '^3.0.0',
|
|
42
47
|
description: 'AWS SDK for SQS',
|
|
43
|
-
installCommand: 'pnpm add @aws-sdk/client-sqs'
|
|
48
|
+
installCommand: 'pnpm add @aws-sdk/client-sqs',
|
|
49
|
+
npmUrl: 'https://www.npmjs.com/package/@aws-sdk/client-sqs'
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
},
|
|
47
53
|
'sqs-consumer': {
|
|
48
54
|
name: 'SQS Queue Consumer',
|
|
55
|
+
docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md',
|
|
49
56
|
dependencies: {
|
|
50
57
|
'@aws-sdk/client-sqs': {
|
|
51
58
|
version: '^3.0.0',
|
|
52
59
|
description: 'AWS SDK for SQS',
|
|
53
|
-
installCommand: 'pnpm add @aws-sdk/client-sqs'
|
|
60
|
+
installCommand: 'pnpm add @aws-sdk/client-sqs',
|
|
61
|
+
npmUrl: 'https://www.npmjs.com/package/@aws-sdk/client-sqs'
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
},
|
|
57
65
|
'rabbitmq-consumer': {
|
|
58
66
|
name: 'RabbitMQ Queue Consumer',
|
|
67
|
+
docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md',
|
|
59
68
|
dependencies: {
|
|
60
69
|
'amqplib': {
|
|
61
70
|
version: '^0.10.0',
|
|
62
71
|
description: 'AMQP 0-9-1 library for RabbitMQ',
|
|
63
|
-
installCommand: 'pnpm add amqplib'
|
|
72
|
+
installCommand: 'pnpm add amqplib',
|
|
73
|
+
npmUrl: 'https://www.npmjs.com/package/amqplib'
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
},
|
|
67
77
|
'tfstate-plugin': {
|
|
68
|
-
name: '
|
|
78
|
+
name: 'Tfstate Plugin',
|
|
79
|
+
docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/tfstate.md',
|
|
69
80
|
dependencies: {
|
|
70
81
|
'node-cron': {
|
|
71
82
|
version: '^4.0.0',
|
|
72
83
|
description: 'Cron job scheduler for auto-sync functionality',
|
|
73
|
-
installCommand: 'pnpm add node-cron'
|
|
84
|
+
installCommand: 'pnpm add node-cron',
|
|
85
|
+
npmUrl: 'https://www.npmjs.com/package/node-cron'
|
|
74
86
|
}
|
|
75
87
|
}
|
|
76
88
|
},
|
|
77
89
|
'api-plugin': {
|
|
78
90
|
name: 'API Plugin',
|
|
91
|
+
docsUrl: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/api.md',
|
|
79
92
|
dependencies: {
|
|
80
93
|
'hono': {
|
|
81
94
|
version: '^4.0.0',
|
|
82
95
|
description: 'Ultra-light HTTP server framework',
|
|
83
|
-
installCommand: 'pnpm add hono'
|
|
96
|
+
installCommand: 'pnpm add hono',
|
|
97
|
+
npmUrl: 'https://www.npmjs.com/package/hono'
|
|
84
98
|
},
|
|
85
99
|
'@hono/node-server': {
|
|
86
100
|
version: '^1.0.0',
|
|
87
101
|
description: 'Node.js adapter for Hono',
|
|
88
|
-
installCommand: 'pnpm add @hono/node-server'
|
|
102
|
+
installCommand: 'pnpm add @hono/node-server',
|
|
103
|
+
npmUrl: 'https://www.npmjs.com/package/@hono/node-server'
|
|
89
104
|
},
|
|
90
105
|
'@hono/swagger-ui': {
|
|
91
106
|
version: '^0.4.0',
|
|
92
107
|
description: 'Swagger UI integration for Hono',
|
|
93
|
-
installCommand: 'pnpm add @hono/swagger-ui'
|
|
108
|
+
installCommand: 'pnpm add @hono/swagger-ui',
|
|
109
|
+
npmUrl: 'https://www.npmjs.com/package/@hono/swagger-ui'
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
}
|
|
@@ -229,22 +245,60 @@ export async function requirePluginDependency(pluginId, options = {}) {
|
|
|
229
245
|
|
|
230
246
|
// Throw comprehensive error if validation failed
|
|
231
247
|
if (!valid && throwOnError) {
|
|
248
|
+
const depCount = Object.keys(pluginDef.dependencies).length;
|
|
249
|
+
const missingCount = missing.length;
|
|
250
|
+
const incompatCount = incompatible.length;
|
|
251
|
+
|
|
232
252
|
const errorMsg = [
|
|
233
|
-
`\n${pluginDef.name} - Missing dependencies detected!\n`,
|
|
234
|
-
`Plugin ID: ${pluginId}`,
|
|
235
253
|
'',
|
|
254
|
+
'╔══════════════════════════════════════════════════════════════════════╗',
|
|
255
|
+
`║ ❌ ${pluginDef.name} - Missing Dependencies ║`,
|
|
256
|
+
'╚══════════════════════════════════════════════════════════════════════╝',
|
|
257
|
+
'',
|
|
258
|
+
`📦 Plugin: ${pluginId}`,
|
|
259
|
+
`📊 Status: ${depCount - missingCount - incompatCount}/${depCount} dependencies satisfied`,
|
|
260
|
+
'',
|
|
261
|
+
'🔍 Dependency Status:',
|
|
262
|
+
'─────────────────────────────────────────────────────────────────────',
|
|
236
263
|
...messages,
|
|
237
264
|
'',
|
|
238
|
-
'Quick
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
265
|
+
'🚀 Quick Fix - Install Missing Dependencies:',
|
|
266
|
+
'─────────────────────────────────────────────────────────────────────',
|
|
267
|
+
'',
|
|
268
|
+
' Option 1: Install individually',
|
|
269
|
+
...Object.entries(pluginDef.dependencies)
|
|
270
|
+
.filter(([pkg]) => missing.includes(pkg) || incompatible.includes(pkg))
|
|
271
|
+
.map(([pkg, info]) => ` ${info.installCommand}`),
|
|
272
|
+
'',
|
|
273
|
+
' Option 2: Install all at once',
|
|
274
|
+
` pnpm add ${Object.keys(pluginDef.dependencies).join(' ')}`,
|
|
242
275
|
'',
|
|
243
|
-
'
|
|
244
|
-
`
|
|
276
|
+
'📚 Documentation:',
|
|
277
|
+
` ${pluginDef.docsUrl}`,
|
|
278
|
+
'',
|
|
279
|
+
'💡 Troubleshooting:',
|
|
280
|
+
' • If packages are installed but not detected, try:',
|
|
281
|
+
' 1. Delete node_modules and reinstall: rm -rf node_modules && pnpm install',
|
|
282
|
+
' 2. Check Node.js version: node --version (requires Node 18+)',
|
|
283
|
+
' 3. Verify pnpm version: pnpm --version (requires pnpm 8+)',
|
|
284
|
+
'',
|
|
285
|
+
' • Still having issues? Check:',
|
|
286
|
+
' - Package.json has correct dependencies listed',
|
|
287
|
+
' - No conflicting versions in pnpm-lock.yaml',
|
|
288
|
+
' - File permissions (especially in node_modules/)',
|
|
289
|
+
'',
|
|
290
|
+
'═══════════════════════════════════════════════════════════════════════',
|
|
291
|
+
''
|
|
245
292
|
].join('\n');
|
|
246
293
|
|
|
247
|
-
|
|
294
|
+
const error = new Error(errorMsg);
|
|
295
|
+
error.pluginId = pluginId;
|
|
296
|
+
error.pluginName = pluginDef.name;
|
|
297
|
+
error.missing = missing;
|
|
298
|
+
error.incompatible = incompatible;
|
|
299
|
+
error.docsUrl = pluginDef.docsUrl;
|
|
300
|
+
|
|
301
|
+
throw error;
|
|
248
302
|
}
|
|
249
303
|
|
|
250
304
|
return { valid, missing, incompatible, messages };
|
|
@@ -559,8 +559,6 @@ async function getAnalyticsForRecord(resourceName, field, recordId, options, han
|
|
|
559
559
|
return [];
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
-
// ✅ FIX BUG #2: Ensure all transactions have cohortHour calculated
|
|
563
|
-
// This handles legacy data that may be missing cohortHour
|
|
564
562
|
allTransactions = ensureCohortHours(allTransactions, handler.config?.cohort?.timezone || 'UTC', false);
|
|
565
563
|
|
|
566
564
|
// Filter transactions by temporal range
|
|
@@ -286,9 +286,7 @@ export async function consolidateRecord(
|
|
|
286
286
|
|
|
287
287
|
if (!hasSetInApplied) {
|
|
288
288
|
// No 'set' operation in applied transactions means we're missing the base value
|
|
289
|
-
// This can
|
|
290
|
-
// 1. Record had an initial value before first transaction
|
|
291
|
-
// 2. First consolidation didn't create an anchor transaction (legacy behavior)
|
|
289
|
+
// This can happen if record had an initial value before first transaction
|
|
292
290
|
// Solution: Get the current record value and create an anchor transaction now
|
|
293
291
|
const recordValue = recordExists[config.field] || 0;
|
|
294
292
|
|
|
@@ -525,7 +523,6 @@ export async function consolidateRecord(
|
|
|
525
523
|
updateResult = result[2];
|
|
526
524
|
}
|
|
527
525
|
|
|
528
|
-
// For backward compatibility, return the value of the main field
|
|
529
526
|
const consolidatedValue = consolidatedValues[config.field] ||
|
|
530
527
|
(record ? lodash.get(record, config.field, 0) : 0);
|
|
531
528
|
|
|
@@ -610,13 +607,10 @@ export async function consolidateRecord(
|
|
|
610
607
|
|
|
611
608
|
const { results, errors } = await PromisePool
|
|
612
609
|
.for(transactionsToUpdate)
|
|
613
|
-
.withConcurrency(markAppliedConcurrency)
|
|
610
|
+
.withConcurrency(markAppliedConcurrency)
|
|
614
611
|
.process(async (txn) => {
|
|
615
|
-
// ✅ FIX BUG #3: Ensure cohort fields exist before marking as applied
|
|
616
|
-
// This handles legacy transactions missing cohortHour, cohortDate, etc.
|
|
617
612
|
const txnWithCohorts = ensureCohortHour(txn, config.cohort.timezone, false);
|
|
618
613
|
|
|
619
|
-
// Build update data with applied flag
|
|
620
614
|
const updateData = { applied: true };
|
|
621
615
|
|
|
622
616
|
// Add missing cohort fields if they were calculated
|
|
@@ -633,11 +627,6 @@ export async function consolidateRecord(
|
|
|
633
627
|
updateData.cohortMonth = txnWithCohorts.cohortMonth;
|
|
634
628
|
}
|
|
635
629
|
|
|
636
|
-
// Handle null value field (legacy data might have null)
|
|
637
|
-
if (txn.value === null || txn.value === undefined) {
|
|
638
|
-
updateData.value = 1; // Default to 1 for backward compatibility
|
|
639
|
-
}
|
|
640
|
-
|
|
641
630
|
const [ok, err] = await tryFn(() =>
|
|
642
631
|
transactionResource.update(txn.id, updateData)
|
|
643
632
|
);
|
|
@@ -183,7 +183,6 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
183
183
|
* @private
|
|
184
184
|
*/
|
|
185
185
|
async _syncModeConsolidate(handler, id, field) {
|
|
186
|
-
// Temporarily set config for legacy methods
|
|
187
186
|
const oldResource = this.config.resource;
|
|
188
187
|
const oldField = this.config.field;
|
|
189
188
|
const oldTransactionResource = this.transactionResource;
|
|
@@ -100,7 +100,7 @@ export async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
100
100
|
operation: 'string|required',
|
|
101
101
|
timestamp: 'string|required',
|
|
102
102
|
cohortDate: 'string|required',
|
|
103
|
-
cohortHour: 'string|
|
|
103
|
+
cohortHour: 'string|required',
|
|
104
104
|
cohortWeek: 'string|optional',
|
|
105
105
|
cohortMonth: 'string|optional',
|
|
106
106
|
source: 'string|optional',
|
|
@@ -127,7 +127,7 @@ class GeoPlugin extends Plugin {
|
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
const resource = this.database.
|
|
130
|
+
const resource = this.database.resources[resourceName];
|
|
131
131
|
if (!resource || typeof resource.addHook !== 'function') {
|
|
132
132
|
if (this.verbose) {
|
|
133
133
|
console.warn(`[GeoPlugin] Resource "${resourceName}" not found or invalid`);
|
|
@@ -325,14 +325,13 @@ class GeoPlugin extends Plugin {
|
|
|
325
325
|
* Find nearby locations within radius
|
|
326
326
|
* Automatically selects optimal zoom level if multi-zoom enabled
|
|
327
327
|
*/
|
|
328
|
-
resource.findNearby = async function({ lat, lon,
|
|
329
|
-
|
|
330
|
-
const longitude = lon !== undefined ? lon : lng;
|
|
331
|
-
|
|
332
|
-
if (lat === undefined || longitude === undefined) {
|
|
328
|
+
resource.findNearby = async function({ lat, lon, radius = 10, limit = 100 }) {
|
|
329
|
+
if (lat === undefined || lon === undefined) {
|
|
333
330
|
throw new Error('lat and lon are required for findNearby');
|
|
334
331
|
}
|
|
335
332
|
|
|
333
|
+
const longitude = lon; // Alias for internal use
|
|
334
|
+
|
|
336
335
|
let allRecords = [];
|
|
337
336
|
|
|
338
337
|
// Use partitions if enabled for efficient queries
|
|
@@ -739,7 +739,7 @@ export class ImporterPlugin extends Plugin {
|
|
|
739
739
|
async onInstall() {
|
|
740
740
|
// Get resource - database.resource() returns a rejected Promise if not found
|
|
741
741
|
try {
|
|
742
|
-
this.resource = this.database.
|
|
742
|
+
this.resource = this.database.resources[this.resourceName];
|
|
743
743
|
// If resource() returns a Promise, await it
|
|
744
744
|
if (this.resource && typeof this.resource.then === 'function') {
|
|
745
745
|
this.resource = await this.resource;
|
|
@@ -416,7 +416,7 @@ class RelationPlugin extends Plugin {
|
|
|
416
416
|
* @private
|
|
417
417
|
*/
|
|
418
418
|
async _setupResourceRelations(resourceName, relationsDef) {
|
|
419
|
-
const resource = this.database.
|
|
419
|
+
const resource = this.database.resources[resourceName];
|
|
420
420
|
if (!resource) {
|
|
421
421
|
if (this.verbose) {
|
|
422
422
|
console.warn(`[RelationPlugin] Resource "${resourceName}" not found, will setup when created`);
|
|
@@ -572,7 +572,7 @@ class RelationPlugin extends Plugin {
|
|
|
572
572
|
for (const record of records) {
|
|
573
573
|
const relatedData = record[relationName];
|
|
574
574
|
if (relatedData) {
|
|
575
|
-
const relatedResource = this.database.
|
|
575
|
+
const relatedResource = this.database.resources[config.resource];
|
|
576
576
|
const relatedArray = Array.isArray(relatedData) ? relatedData : [relatedData];
|
|
577
577
|
|
|
578
578
|
if (relatedArray.length > 0) {
|
|
@@ -635,7 +635,7 @@ class RelationPlugin extends Plugin {
|
|
|
635
635
|
* @private
|
|
636
636
|
*/
|
|
637
637
|
async _loadHasOne(records, relationName, config, sourceResource) {
|
|
638
|
-
const relatedResource = this.database.
|
|
638
|
+
const relatedResource = this.database.resources[config.resource];
|
|
639
639
|
if (!relatedResource) {
|
|
640
640
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
641
641
|
sourceResource: sourceResource.name,
|
|
@@ -689,7 +689,7 @@ class RelationPlugin extends Plugin {
|
|
|
689
689
|
* @private
|
|
690
690
|
*/
|
|
691
691
|
async _loadHasMany(records, relationName, config, sourceResource) {
|
|
692
|
-
const relatedResource = this.database.
|
|
692
|
+
const relatedResource = this.database.resources[config.resource];
|
|
693
693
|
if (!relatedResource) {
|
|
694
694
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
695
695
|
sourceResource: sourceResource.name,
|
|
@@ -751,7 +751,7 @@ class RelationPlugin extends Plugin {
|
|
|
751
751
|
* @private
|
|
752
752
|
*/
|
|
753
753
|
async _loadBelongsTo(records, relationName, config, sourceResource) {
|
|
754
|
-
const relatedResource = this.database.
|
|
754
|
+
const relatedResource = this.database.resources[config.resource];
|
|
755
755
|
if (!relatedResource) {
|
|
756
756
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
757
757
|
sourceResource: sourceResource.name,
|
|
@@ -818,7 +818,7 @@ class RelationPlugin extends Plugin {
|
|
|
818
818
|
* @private
|
|
819
819
|
*/
|
|
820
820
|
async _loadBelongsToMany(records, relationName, config, sourceResource) {
|
|
821
|
-
const relatedResource = this.database.
|
|
821
|
+
const relatedResource = this.database.resources[config.resource];
|
|
822
822
|
if (!relatedResource) {
|
|
823
823
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
824
824
|
sourceResource: sourceResource.name,
|
|
@@ -826,7 +826,7 @@ class RelationPlugin extends Plugin {
|
|
|
826
826
|
});
|
|
827
827
|
}
|
|
828
828
|
|
|
829
|
-
const junctionResource = this.database.
|
|
829
|
+
const junctionResource = this.database.resources[config.through];
|
|
830
830
|
if (!junctionResource) {
|
|
831
831
|
throw new JunctionTableNotFoundError(config.through, {
|
|
832
832
|
sourceResource: sourceResource.name,
|
|
@@ -1077,7 +1077,7 @@ class RelationPlugin extends Plugin {
|
|
|
1077
1077
|
async _cascadeDelete(record, resource, relationName, config) {
|
|
1078
1078
|
this.stats.cascadeOperations++;
|
|
1079
1079
|
|
|
1080
|
-
const relatedResource = this.database.
|
|
1080
|
+
const relatedResource = this.database.resources[config.resource];
|
|
1081
1081
|
if (!relatedResource) {
|
|
1082
1082
|
throw new RelatedResourceNotFoundError(config.resource, {
|
|
1083
1083
|
sourceResource: resource.name,
|
|
@@ -1087,7 +1087,7 @@ class RelationPlugin extends Plugin {
|
|
|
1087
1087
|
|
|
1088
1088
|
// Track deleted records for rollback (if transactions enabled)
|
|
1089
1089
|
const deletedRecords = [];
|
|
1090
|
-
const junctionResource = config.type === 'belongsToMany' ? this.database.
|
|
1090
|
+
const junctionResource = config.type === 'belongsToMany' ? this.database.resources[config.through] : null;
|
|
1091
1091
|
|
|
1092
1092
|
try {
|
|
1093
1093
|
if (config.type === 'hasMany') {
|
|
@@ -1156,7 +1156,7 @@ class RelationPlugin extends Plugin {
|
|
|
1156
1156
|
}
|
|
1157
1157
|
} else if (config.type === 'belongsToMany') {
|
|
1158
1158
|
// Delete junction table entries - use partition if available
|
|
1159
|
-
const junctionResource = this.database.
|
|
1159
|
+
const junctionResource = this.database.resources[config.through];
|
|
1160
1160
|
if (junctionResource) {
|
|
1161
1161
|
let junctionRecords;
|
|
1162
1162
|
const partitionName = this._findPartitionByField(junctionResource, config.foreignKey);
|
|
@@ -1240,7 +1240,7 @@ class RelationPlugin extends Plugin {
|
|
|
1240
1240
|
async _cascadeUpdate(record, changes, resource, relationName, config) {
|
|
1241
1241
|
this.stats.cascadeOperations++;
|
|
1242
1242
|
|
|
1243
|
-
const relatedResource = this.database.
|
|
1243
|
+
const relatedResource = this.database.resources[config.resource];
|
|
1244
1244
|
if (!relatedResource) {
|
|
1245
1245
|
return;
|
|
1246
1246
|
}
|
|
@@ -306,18 +306,6 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
306
306
|
// Plugin is ready
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
async stop() {
|
|
310
|
-
// Stop all replicators
|
|
311
|
-
for (const replicator of this.replicators || []) {
|
|
312
|
-
if (replicator && typeof replicator.cleanup === 'function') {
|
|
313
|
-
await replicator.cleanup();
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Remove database hooks
|
|
318
|
-
this.removeDatabaseHooks();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
309
|
installDatabaseHooks() {
|
|
322
310
|
// Store hook reference for later removal
|
|
323
311
|
this._afterCreateResourceHook = (resource) => {
|
|
@@ -730,21 +718,21 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
730
718
|
this.emit('replicator.sync.completed', { replicatorId, stats: this.stats });
|
|
731
719
|
}
|
|
732
720
|
|
|
733
|
-
async
|
|
721
|
+
async stop() {
|
|
734
722
|
const [ok, error] = await tryFn(async () => {
|
|
735
723
|
if (this.replicators && this.replicators.length > 0) {
|
|
736
724
|
const cleanupPromises = this.replicators.map(async (replicator) => {
|
|
737
725
|
const [replicatorOk, replicatorError] = await tryFn(async () => {
|
|
738
|
-
if (replicator && typeof replicator.
|
|
739
|
-
await replicator.
|
|
726
|
+
if (replicator && typeof replicator.stop === 'function') {
|
|
727
|
+
await replicator.stop();
|
|
740
728
|
}
|
|
741
729
|
});
|
|
742
|
-
|
|
730
|
+
|
|
743
731
|
if (!replicatorOk) {
|
|
744
732
|
if (this.config.verbose) {
|
|
745
|
-
console.warn(`[ReplicatorPlugin] Failed to
|
|
733
|
+
console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
|
|
746
734
|
}
|
|
747
|
-
this.emit('
|
|
735
|
+
this.emit('replicator_stop_error', {
|
|
748
736
|
replicator: replicator.name || replicator.id || 'unknown',
|
|
749
737
|
driver: replicator.driver || 'unknown',
|
|
750
738
|
error: replicatorError.message
|
|
@@ -754,7 +742,10 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
754
742
|
|
|
755
743
|
await Promise.allSettled(cleanupPromises);
|
|
756
744
|
}
|
|
757
|
-
|
|
745
|
+
|
|
746
|
+
// Remove database hooks
|
|
747
|
+
this.removeDatabaseHooks();
|
|
748
|
+
|
|
758
749
|
// Remove event listeners from resources to prevent memory leaks
|
|
759
750
|
if (this.database && this.database.resources) {
|
|
760
751
|
for (const resourceName of this.eventListenersInstalled) {
|
|
@@ -779,9 +770,9 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
779
770
|
|
|
780
771
|
if (!ok) {
|
|
781
772
|
if (this.config.verbose) {
|
|
782
|
-
console.warn(`[ReplicatorPlugin] Failed to
|
|
773
|
+
console.warn(`[ReplicatorPlugin] Failed to stop plugin: ${error.message}`);
|
|
783
774
|
}
|
|
784
|
-
this.emit('
|
|
775
|
+
this.emit('replicator_plugin_stop_error', {
|
|
785
776
|
error: error.message
|
|
786
777
|
});
|
|
787
778
|
}
|
|
@@ -42,22 +42,22 @@ import { idGenerator } from "../concerns/id.js";
|
|
|
42
42
|
* === Usage ===
|
|
43
43
|
*
|
|
44
44
|
* // Enqueue a message
|
|
45
|
-
* await db.
|
|
45
|
+
* await db.resources.emails.enqueue({
|
|
46
46
|
* to: 'user@example.com',
|
|
47
47
|
* subject: 'Hello',
|
|
48
48
|
* body: 'World'
|
|
49
49
|
* });
|
|
50
50
|
*
|
|
51
51
|
* // Start processing (if not auto-started)
|
|
52
|
-
* await db.
|
|
52
|
+
* await db.resources.emails.startProcessing(async (email) => {
|
|
53
53
|
* await sendEmail(email);
|
|
54
54
|
* }, { concurrency: 10 });
|
|
55
55
|
*
|
|
56
56
|
* // Stop processing
|
|
57
|
-
* await db.
|
|
57
|
+
* await db.resources.emails.stopProcessing();
|
|
58
58
|
*
|
|
59
59
|
* // Get queue statistics
|
|
60
|
-
* const stats = await db.
|
|
60
|
+
* const stats = await db.resources.emails.queueStats();
|
|
61
61
|
* // { total: 100, pending: 50, processing: 20, completed: 25, failed: 5, dead: 0 }
|
|
62
62
|
*/
|
|
63
63
|
export class S3QueuePlugin extends Plugin {
|
|
@@ -30,11 +30,11 @@ import { SchedulerError } from "./scheduler.errors.js";
|
|
|
30
30
|
* schedule: '0 3 * * *',
|
|
31
31
|
* description: 'Clean up expired records',
|
|
32
32
|
* action: async (database, context) => {
|
|
33
|
-
* const expired = await this.database.
|
|
33
|
+
* const expired = await this.database.resources['sessions')
|
|
34
34
|
* .list({ where: { expiresAt: { $lt: new Date() } } });
|
|
35
35
|
*
|
|
36
36
|
* for (const record of expired) {
|
|
37
|
-
* await this.database.
|
|
37
|
+
* await this.database.resources['sessions').delete(record.id);
|
|
38
38
|
* }
|
|
39
39
|
*
|
|
40
40
|
* return { deleted: expired.length };
|
|
@@ -49,8 +49,8 @@ import { SchedulerError } from "./scheduler.errors.js";
|
|
|
49
49
|
* schedule: '0 9 * * MON',
|
|
50
50
|
* description: 'Generate weekly analytics report',
|
|
51
51
|
* action: async (database, context) => {
|
|
52
|
-
* const users = await this.database.
|
|
53
|
-
* const orders = await this.database.
|
|
52
|
+
* const users = await this.database.resources['users').count();
|
|
53
|
+
* const orders = await this.database.resources['orders').count({
|
|
54
54
|
* where: {
|
|
55
55
|
* createdAt: {
|
|
56
56
|
* $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
@@ -65,7 +65,7 @@ import { SchedulerError } from "./scheduler.errors.js";
|
|
|
65
65
|
* createdAt: new Date().toISOString()
|
|
66
66
|
* };
|
|
67
67
|
*
|
|
68
|
-
* await this.database.
|
|
68
|
+
* await this.database.resources['reports').insert(report);
|
|
69
69
|
* return report;
|
|
70
70
|
* }
|
|
71
71
|
* },
|
|
@@ -107,7 +107,7 @@ import { SchedulerError } from "./scheduler.errors.js";
|
|
|
107
107
|
* const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
108
108
|
*
|
|
109
109
|
* // Aggregate metrics from the last hour
|
|
110
|
-
* const events = await this.database.
|
|
110
|
+
* const events = await this.database.resources['events').list({
|
|
111
111
|
* where: {
|
|
112
112
|
* timestamp: {
|
|
113
113
|
* $gte: hourAgo.getTime(),
|
|
@@ -121,7 +121,7 @@ import { SchedulerError } from "./scheduler.errors.js";
|
|
|
121
121
|
* return acc;
|
|
122
122
|
* }, {});
|
|
123
123
|
*
|
|
124
|
-
* await this.database.
|
|
124
|
+
* await this.database.resources['hourly_metrics').insert({
|
|
125
125
|
* hour: hourAgo.toISOString().slice(0, 13),
|
|
126
126
|
* metrics: aggregated,
|
|
127
127
|
* total: events.length,
|
|
@@ -577,7 +577,7 @@ export class SchedulerPlugin extends Plugin {
|
|
|
577
577
|
|
|
578
578
|
async _persistJobExecution(jobName, executionId, startTime, endTime, duration, status, result, error, retryCount) {
|
|
579
579
|
const [ok, err] = await tryFn(() =>
|
|
580
|
-
this.database.
|
|
580
|
+
this.database.resources[this.config.jobHistoryResource].insert({
|
|
581
581
|
id: executionId,
|
|
582
582
|
jobName,
|
|
583
583
|
status,
|
|
@@ -741,7 +741,7 @@ export class SchedulerPlugin extends Plugin {
|
|
|
741
741
|
|
|
742
742
|
// Use query() to leverage partitions instead of list() + filter
|
|
743
743
|
const [ok, err, history] = await tryFn(() =>
|
|
744
|
-
this.database.
|
|
744
|
+
this.database.resources[this.config.jobHistoryResource].query(queryParams)
|
|
745
745
|
);
|
|
746
746
|
|
|
747
747
|
if (!ok) {
|
|
@@ -914,10 +914,8 @@ export class SchedulerPlugin extends Plugin {
|
|
|
914
914
|
if (this._isTestEnvironment()) {
|
|
915
915
|
this.activeJobs.clear();
|
|
916
916
|
}
|
|
917
|
-
}
|
|
918
917
|
|
|
919
|
-
|
|
920
|
-
await this.stop();
|
|
918
|
+
// Cleanup resources
|
|
921
919
|
this.jobs.clear();
|
|
922
920
|
this.statistics.clear();
|
|
923
921
|
this.activeJobs.clear();
|
|
@@ -62,7 +62,7 @@ import { StateMachineError } from "./state-machine.errors.js";
|
|
|
62
62
|
*
|
|
63
63
|
* actions: {
|
|
64
64
|
* onConfirmed: async (context, event, machine) => {
|
|
65
|
-
* await machine.this.database.
|
|
65
|
+
* await machine.this.database.resources['inventory'].update(context.productId, {
|
|
66
66
|
* quantity: { $decrement: context.quantity }
|
|
67
67
|
* });
|
|
68
68
|
* await machine.sendNotification(context.customerEmail, 'order_confirmed');
|
|
@@ -74,7 +74,7 @@ import { StateMachineError } from "./state-machine.errors.js";
|
|
|
74
74
|
*
|
|
75
75
|
* guards: {
|
|
76
76
|
* canShip: async (context, event, machine) => {
|
|
77
|
-
* const inventory = await machine.this.database.
|
|
77
|
+
* const inventory = await machine.this.database.resources['inventory'].get(context.productId);
|
|
78
78
|
* return inventory.quantity >= context.quantity;
|
|
79
79
|
* }
|
|
80
80
|
* },
|
|
@@ -352,7 +352,7 @@ export class StateMachinePlugin extends Plugin {
|
|
|
352
352
|
|
|
353
353
|
for (let attempt = 0; attempt < this.config.retryAttempts; attempt++) {
|
|
354
354
|
const [ok, err] = await tryFn(() =>
|
|
355
|
-
this.database.
|
|
355
|
+
this.database.resources[this.config.transitionLogResource].insert({
|
|
356
356
|
id: transitionId,
|
|
357
357
|
machineId,
|
|
358
358
|
entityId,
|
|
@@ -395,13 +395,13 @@ export class StateMachinePlugin extends Plugin {
|
|
|
395
395
|
|
|
396
396
|
// Try update first (most common case), fallback to insert if doesn't exist
|
|
397
397
|
const [updateOk] = await tryFn(() =>
|
|
398
|
-
this.database.
|
|
398
|
+
this.database.resources[this.config.stateResource].update(stateId, stateData)
|
|
399
399
|
);
|
|
400
400
|
|
|
401
401
|
if (!updateOk) {
|
|
402
402
|
// Record doesn't exist, insert it
|
|
403
403
|
const [insertOk, insertErr] = await tryFn(() =>
|
|
404
|
-
this.database.
|
|
404
|
+
this.database.resources[this.config.stateResource].insert({ id: stateId, ...stateData })
|
|
405
405
|
);
|
|
406
406
|
|
|
407
407
|
if (!insertOk && this.config.verbose) {
|
|
@@ -476,7 +476,7 @@ export class StateMachinePlugin extends Plugin {
|
|
|
476
476
|
if (this.config.persistTransitions) {
|
|
477
477
|
const stateId = `${machineId}_${entityId}`;
|
|
478
478
|
const [ok, err, stateRecord] = await tryFn(() =>
|
|
479
|
-
this.database.
|
|
479
|
+
this.database.resources[this.config.stateResource].get(stateId)
|
|
480
480
|
);
|
|
481
481
|
|
|
482
482
|
if (ok && stateRecord) {
|
|
@@ -530,7 +530,7 @@ export class StateMachinePlugin extends Plugin {
|
|
|
530
530
|
const { limit = 50, offset = 0 } = options;
|
|
531
531
|
|
|
532
532
|
const [ok, err, transitions] = await tryFn(() =>
|
|
533
|
-
this.database.
|
|
533
|
+
this.database.resources[this.config.transitionLogResource].query({
|
|
534
534
|
machineId,
|
|
535
535
|
entityId
|
|
536
536
|
}, {
|
|
@@ -581,7 +581,7 @@ export class StateMachinePlugin extends Plugin {
|
|
|
581
581
|
|
|
582
582
|
// Try to insert, ignore if already exists (idempotent)
|
|
583
583
|
const [ok, err] = await tryFn(() =>
|
|
584
|
-
this.database.
|
|
584
|
+
this.database.resources[this.config.stateResource].insert({
|
|
585
585
|
id: stateId,
|
|
586
586
|
machineId,
|
|
587
587
|
entityId,
|
|
@@ -682,10 +682,6 @@ export class StateMachinePlugin extends Plugin {
|
|
|
682
682
|
|
|
683
683
|
async stop() {
|
|
684
684
|
this.machines.clear();
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
async cleanup() {
|
|
688
|
-
await this.stop();
|
|
689
685
|
this.removeAllListeners();
|
|
690
686
|
}
|
|
691
687
|
}
|
|
@@ -740,6 +740,6 @@ Verificar que partitions são rápidas:
|
|
|
740
740
|
|
|
741
741
|
## 📚 References
|
|
742
742
|
|
|
743
|
-
- [
|
|
743
|
+
- [Tfstate Format](https://www.terraform.io/internals/json-format)
|
|
744
744
|
- [s3db Partitioning Guide](../../docs/partitioning.md)
|
|
745
745
|
- [Plugin Development](../../docs/plugins.md)
|