s3db.js 11.0.2 → 11.0.4
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 +612 -308
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +612 -308
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/concerns/plugin-storage.js +274 -9
- package/src/plugins/audit.plugin.js +94 -18
- package/src/plugins/eventual-consistency/analytics.js +350 -15
- package/src/plugins/eventual-consistency/config.js +3 -0
- package/src/plugins/eventual-consistency/consolidation.js +32 -36
- package/src/plugins/eventual-consistency/garbage-collection.js +11 -13
- package/src/plugins/eventual-consistency/index.js +28 -19
- package/src/plugins/eventual-consistency/install.js +9 -26
- package/src/plugins/eventual-consistency/partitions.js +5 -0
- package/src/plugins/eventual-consistency/transactions.js +1 -0
- package/src/plugins/eventual-consistency/utils.js +36 -1
- package/src/plugins/fulltext.plugin.js +76 -22
- package/src/plugins/metrics.plugin.js +70 -20
- package/src/plugins/s3-queue.plugin.js +21 -120
- package/src/plugins/scheduler.plugin.js +11 -37
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
runConsolidation
|
|
18
18
|
} from "./consolidation.js";
|
|
19
19
|
import { runGarbageCollection } from "./garbage-collection.js";
|
|
20
|
-
import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getMonthByHour, getTopRecords } from "./analytics.js";
|
|
20
|
+
import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getYearByWeek, getMonthByWeek, getMonthByHour, getTopRecords } from "./analytics.js";
|
|
21
21
|
import { onInstall, onStart, onStop, watchForResource, completeFieldSetup } from "./install.js";
|
|
22
22
|
|
|
23
23
|
export class EventualConsistencyPlugin extends Plugin {
|
|
@@ -125,7 +125,7 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
125
125
|
originalId,
|
|
126
126
|
this.transactionResource,
|
|
127
127
|
this.targetResource,
|
|
128
|
-
this.
|
|
128
|
+
this.getStorage(),
|
|
129
129
|
this.analyticsResource,
|
|
130
130
|
(transactions) => this.updateAnalytics(transactions),
|
|
131
131
|
this.config
|
|
@@ -164,7 +164,7 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
164
164
|
originalId,
|
|
165
165
|
this.transactionResource,
|
|
166
166
|
this.targetResource,
|
|
167
|
-
this.
|
|
167
|
+
this.getStorage(),
|
|
168
168
|
(id) => this.consolidateRecord(id),
|
|
169
169
|
this.config
|
|
170
170
|
);
|
|
@@ -188,14 +188,12 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
188
188
|
const oldField = this.config.field;
|
|
189
189
|
const oldTransactionResource = this.transactionResource;
|
|
190
190
|
const oldTargetResource = this.targetResource;
|
|
191
|
-
const oldLockResource = this.lockResource;
|
|
192
191
|
const oldAnalyticsResource = this.analyticsResource;
|
|
193
192
|
|
|
194
193
|
this.config.resource = handler.resource;
|
|
195
194
|
this.config.field = handler.field;
|
|
196
195
|
this.transactionResource = handler.transactionResource;
|
|
197
196
|
this.targetResource = handler.targetResource;
|
|
198
|
-
this.lockResource = handler.lockResource;
|
|
199
197
|
this.analyticsResource = handler.analyticsResource;
|
|
200
198
|
|
|
201
199
|
const result = await this.consolidateRecord(id);
|
|
@@ -205,7 +203,6 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
205
203
|
this.config.field = oldField;
|
|
206
204
|
this.transactionResource = oldTransactionResource;
|
|
207
205
|
this.targetResource = oldTargetResource;
|
|
208
|
-
this.lockResource = oldLockResource;
|
|
209
206
|
this.analyticsResource = oldAnalyticsResource;
|
|
210
207
|
|
|
211
208
|
return result;
|
|
@@ -220,14 +217,12 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
220
217
|
const oldField = this.config.field;
|
|
221
218
|
const oldTransactionResource = this.transactionResource;
|
|
222
219
|
const oldTargetResource = this.targetResource;
|
|
223
|
-
const oldLockResource = this.lockResource;
|
|
224
220
|
const oldAnalyticsResource = this.analyticsResource;
|
|
225
221
|
|
|
226
222
|
this.config.resource = handler.resource;
|
|
227
223
|
this.config.field = handler.field;
|
|
228
224
|
this.transactionResource = handler.transactionResource;
|
|
229
225
|
this.targetResource = handler.targetResource;
|
|
230
|
-
this.lockResource = handler.lockResource;
|
|
231
226
|
this.analyticsResource = handler.analyticsResource;
|
|
232
227
|
|
|
233
228
|
const result = await this.consolidateRecord(id);
|
|
@@ -236,7 +231,6 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
236
231
|
this.config.field = oldField;
|
|
237
232
|
this.transactionResource = oldTransactionResource;
|
|
238
233
|
this.targetResource = oldTargetResource;
|
|
239
|
-
this.lockResource = oldLockResource;
|
|
240
234
|
this.analyticsResource = oldAnalyticsResource;
|
|
241
235
|
|
|
242
236
|
return result;
|
|
@@ -276,14 +270,12 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
276
270
|
const oldField = this.config.field;
|
|
277
271
|
const oldTransactionResource = this.transactionResource;
|
|
278
272
|
const oldTargetResource = this.targetResource;
|
|
279
|
-
const oldLockResource = this.lockResource;
|
|
280
273
|
const oldAnalyticsResource = this.analyticsResource;
|
|
281
274
|
|
|
282
275
|
this.config.resource = handler.resource;
|
|
283
276
|
this.config.field = handler.field;
|
|
284
277
|
this.transactionResource = handler.transactionResource;
|
|
285
278
|
this.targetResource = handler.targetResource;
|
|
286
|
-
this.lockResource = handler.lockResource;
|
|
287
279
|
this.analyticsResource = handler.analyticsResource;
|
|
288
280
|
|
|
289
281
|
const result = await this.recalculateRecord(id);
|
|
@@ -292,7 +284,6 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
292
284
|
this.config.field = oldField;
|
|
293
285
|
this.transactionResource = oldTransactionResource;
|
|
294
286
|
this.targetResource = oldTargetResource;
|
|
295
|
-
this.lockResource = oldLockResource;
|
|
296
287
|
this.analyticsResource = oldAnalyticsResource;
|
|
297
288
|
|
|
298
289
|
return result;
|
|
@@ -307,14 +298,12 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
307
298
|
const oldField = this.config.field;
|
|
308
299
|
const oldTransactionResource = this.transactionResource;
|
|
309
300
|
const oldTargetResource = this.targetResource;
|
|
310
|
-
const oldLockResource = this.lockResource;
|
|
311
301
|
const oldAnalyticsResource = this.analyticsResource;
|
|
312
302
|
|
|
313
303
|
this.config.resource = resourceName;
|
|
314
304
|
this.config.field = fieldName;
|
|
315
305
|
this.transactionResource = handler.transactionResource;
|
|
316
306
|
this.targetResource = handler.targetResource;
|
|
317
|
-
this.lockResource = handler.lockResource;
|
|
318
307
|
this.analyticsResource = handler.analyticsResource;
|
|
319
308
|
|
|
320
309
|
try {
|
|
@@ -329,7 +318,6 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
329
318
|
this.config.field = oldField;
|
|
330
319
|
this.transactionResource = oldTransactionResource;
|
|
331
320
|
this.targetResource = oldTargetResource;
|
|
332
|
-
this.lockResource = oldLockResource;
|
|
333
321
|
this.analyticsResource = oldAnalyticsResource;
|
|
334
322
|
}
|
|
335
323
|
}
|
|
@@ -343,18 +331,16 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
343
331
|
const oldField = this.config.field;
|
|
344
332
|
const oldTransactionResource = this.transactionResource;
|
|
345
333
|
const oldTargetResource = this.targetResource;
|
|
346
|
-
const oldLockResource = this.lockResource;
|
|
347
334
|
|
|
348
335
|
this.config.resource = resourceName;
|
|
349
336
|
this.config.field = fieldName;
|
|
350
337
|
this.transactionResource = handler.transactionResource;
|
|
351
338
|
this.targetResource = handler.targetResource;
|
|
352
|
-
this.lockResource = handler.lockResource;
|
|
353
339
|
|
|
354
340
|
try {
|
|
355
341
|
await runGarbageCollection(
|
|
356
342
|
this.transactionResource,
|
|
357
|
-
this.
|
|
343
|
+
this.getStorage(),
|
|
358
344
|
this.config,
|
|
359
345
|
(event, data) => this.emit(event, data)
|
|
360
346
|
);
|
|
@@ -363,7 +349,6 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
363
349
|
this.config.field = oldField;
|
|
364
350
|
this.transactionResource = oldTransactionResource;
|
|
365
351
|
this.targetResource = oldTargetResource;
|
|
366
|
-
this.lockResource = oldLockResource;
|
|
367
352
|
}
|
|
368
353
|
}
|
|
369
354
|
|
|
@@ -440,6 +425,30 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
440
425
|
return await getMonthByHour(resourceName, field, month, options, this.fieldHandlers);
|
|
441
426
|
}
|
|
442
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Get analytics for entire year, broken down by weeks
|
|
430
|
+
* @param {string} resourceName - Resource name
|
|
431
|
+
* @param {string} field - Field name
|
|
432
|
+
* @param {number} year - Year (e.g., 2025)
|
|
433
|
+
* @param {Object} options - Options
|
|
434
|
+
* @returns {Promise<Array>} Weekly analytics for the year (up to 53 weeks)
|
|
435
|
+
*/
|
|
436
|
+
async getYearByWeek(resourceName, field, year, options = {}) {
|
|
437
|
+
return await getYearByWeek(resourceName, field, year, options, this.fieldHandlers);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get analytics for entire month, broken down by weeks
|
|
442
|
+
* @param {string} resourceName - Resource name
|
|
443
|
+
* @param {string} field - Field name
|
|
444
|
+
* @param {string} month - Month in YYYY-MM format
|
|
445
|
+
* @param {Object} options - Options
|
|
446
|
+
* @returns {Promise<Array>} Weekly analytics for the month
|
|
447
|
+
*/
|
|
448
|
+
async getMonthByWeek(resourceName, field, month, options = {}) {
|
|
449
|
+
return await getMonthByWeek(resourceName, field, month, options, this.fieldHandlers);
|
|
450
|
+
}
|
|
451
|
+
|
|
443
452
|
/**
|
|
444
453
|
* Get top records by volume
|
|
445
454
|
* @param {string} resourceName - Resource name
|
|
@@ -84,8 +84,8 @@ export async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
84
84
|
const resourceName = handler.resource;
|
|
85
85
|
const fieldName = handler.field;
|
|
86
86
|
|
|
87
|
-
// Create transaction resource with partitions
|
|
88
|
-
const transactionResourceName =
|
|
87
|
+
// Create transaction resource with partitions (plg_ prefix for plugin resources)
|
|
88
|
+
const transactionResourceName = `plg_${resourceName}_tx_${fieldName}`;
|
|
89
89
|
const partitionConfig = createPartitionConfig();
|
|
90
90
|
|
|
91
91
|
const [ok, err, transactionResource] = await tryFn(() =>
|
|
@@ -100,6 +100,7 @@ export async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
100
100
|
timestamp: 'string|required',
|
|
101
101
|
cohortDate: 'string|required',
|
|
102
102
|
cohortHour: 'string|required',
|
|
103
|
+
cohortWeek: 'string|optional',
|
|
103
104
|
cohortMonth: 'string|optional',
|
|
104
105
|
source: 'string|optional',
|
|
105
106
|
applied: 'boolean|optional'
|
|
@@ -118,27 +119,8 @@ export async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
118
119
|
|
|
119
120
|
handler.transactionResource = ok ? transactionResource : database.resources[transactionResourceName];
|
|
120
121
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
const [lockOk, lockErr, lockResource] = await tryFn(() =>
|
|
124
|
-
database.createResource({
|
|
125
|
-
name: lockResourceName,
|
|
126
|
-
attributes: {
|
|
127
|
-
id: 'string|required',
|
|
128
|
-
lockedAt: 'number|required',
|
|
129
|
-
workerId: 'string|optional'
|
|
130
|
-
},
|
|
131
|
-
behavior: 'body-only',
|
|
132
|
-
timestamps: false,
|
|
133
|
-
createdBy: 'EventualConsistencyPlugin'
|
|
134
|
-
})
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
if (!lockOk && !database.resources[lockResourceName]) {
|
|
138
|
-
throw new Error(`Failed to create lock resource for ${resourceName}.${fieldName}: ${lockErr?.message}`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
handler.lockResource = lockOk ? lockResource : database.resources[lockResourceName];
|
|
122
|
+
// Locks are now managed by PluginStorage with TTL - no Resource needed
|
|
123
|
+
// Lock acquisition is handled via storage.acquireLock() with automatic expiration
|
|
142
124
|
|
|
143
125
|
// Create analytics resource if enabled
|
|
144
126
|
if (config.enableAnalytics) {
|
|
@@ -151,8 +133,9 @@ export async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
151
133
|
if (config.verbose) {
|
|
152
134
|
console.log(
|
|
153
135
|
`[EventualConsistency] ${resourceName}.${fieldName} - ` +
|
|
154
|
-
`Setup complete. Resources: ${transactionResourceName}
|
|
155
|
-
`${config.enableAnalytics ? `, ${resourceName}
|
|
136
|
+
`Setup complete. Resources: ${transactionResourceName}` +
|
|
137
|
+
`${config.enableAnalytics ? `, plg_${resourceName}_an_${fieldName}` : ''}` +
|
|
138
|
+
` (locks via PluginStorage TTL)`
|
|
156
139
|
);
|
|
157
140
|
}
|
|
158
141
|
}
|
|
@@ -167,7 +150,7 @@ export async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
167
150
|
* @returns {Promise<void>}
|
|
168
151
|
*/
|
|
169
152
|
async function createAnalyticsResource(handler, database, resourceName, fieldName) {
|
|
170
|
-
const analyticsResourceName =
|
|
153
|
+
const analyticsResourceName = `plg_${resourceName}_an_${fieldName}`;
|
|
171
154
|
|
|
172
155
|
const [ok, err, analyticsResource] = await tryFn(() =>
|
|
173
156
|
database.createResource({
|
|
@@ -54,6 +54,7 @@ export async function createTransaction(handler, data, config) {
|
|
|
54
54
|
timestamp: now.toISOString(),
|
|
55
55
|
cohortDate: cohortInfo.date,
|
|
56
56
|
cohortHour: cohortInfo.hour,
|
|
57
|
+
cohortWeek: cohortInfo.week,
|
|
57
58
|
cohortMonth: cohortInfo.month,
|
|
58
59
|
source: data.source || 'unknown',
|
|
59
60
|
applied: false
|
|
@@ -64,12 +64,42 @@ export function getTimezoneOffset(timezone, verbose = false) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Calculate ISO 8601 week number for a date
|
|
69
|
+
* @param {Date} date - Date to get week number for
|
|
70
|
+
* @returns {Object} Year and week number { year, week }
|
|
71
|
+
*/
|
|
72
|
+
function getISOWeek(date) {
|
|
73
|
+
// Copy date to avoid mutating original
|
|
74
|
+
const target = new Date(date.valueOf());
|
|
75
|
+
|
|
76
|
+
// ISO week starts on Monday (day 1)
|
|
77
|
+
// Find Thursday of this week (ISO week contains Jan 4th)
|
|
78
|
+
const dayNr = (date.getUTCDay() + 6) % 7; // Make Monday = 0 (use UTC)
|
|
79
|
+
target.setUTCDate(target.getUTCDate() - dayNr + 3); // Thursday of this week
|
|
80
|
+
|
|
81
|
+
// Get first Thursday of the year (use UTC)
|
|
82
|
+
const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1));
|
|
83
|
+
const firstThursday = new Date(yearStart.valueOf());
|
|
84
|
+
if (yearStart.getUTCDay() !== 4) {
|
|
85
|
+
firstThursday.setUTCDate(yearStart.getUTCDate() + ((4 - yearStart.getUTCDay()) + 7) % 7);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Calculate week number
|
|
89
|
+
const weekNumber = 1 + Math.round((target - firstThursday) / 604800000);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
year: target.getUTCFullYear(),
|
|
93
|
+
week: weekNumber
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
67
97
|
/**
|
|
68
98
|
* Get cohort information for a date
|
|
69
99
|
* @param {Date} date - Date to get cohort info for
|
|
70
100
|
* @param {string} timezone - IANA timezone name
|
|
71
101
|
* @param {boolean} verbose - Whether to log warnings
|
|
72
|
-
* @returns {Object} Cohort information (date, hour, month)
|
|
102
|
+
* @returns {Object} Cohort information (date, hour, week, month)
|
|
73
103
|
*/
|
|
74
104
|
export function getCohortInfo(date, timezone, verbose = false) {
|
|
75
105
|
// Simple timezone offset calculation
|
|
@@ -81,9 +111,14 @@ export function getCohortInfo(date, timezone, verbose = false) {
|
|
|
81
111
|
const day = String(localDate.getDate()).padStart(2, '0');
|
|
82
112
|
const hour = String(localDate.getHours()).padStart(2, '0');
|
|
83
113
|
|
|
114
|
+
// Calculate ISO week
|
|
115
|
+
const { year: weekYear, week: weekNumber } = getISOWeek(localDate);
|
|
116
|
+
const week = `${weekYear}-W${String(weekNumber).padStart(2, '0')}`;
|
|
117
|
+
|
|
84
118
|
return {
|
|
85
119
|
date: `${year}-${month}-${day}`,
|
|
86
120
|
hour: `${year}-${month}-${day}T${hour}`, // ISO-like format for hour partition
|
|
121
|
+
week: week, // ISO 8601 week format (e.g., '2025-W42')
|
|
87
122
|
month: `${year}-${month}`
|
|
88
123
|
};
|
|
89
124
|
}
|
|
@@ -11,6 +11,8 @@ export class FullTextPlugin extends Plugin {
|
|
|
11
11
|
...options
|
|
12
12
|
};
|
|
13
13
|
this.indexes = new Map(); // In-memory index for simplicity
|
|
14
|
+
this.dirtyIndexes = new Set(); // Track changed index keys for incremental saves
|
|
15
|
+
this.deletedIndexes = new Set(); // Track deleted index keys
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
async onInstall() {
|
|
@@ -26,7 +28,11 @@ export class FullTextPlugin extends Plugin {
|
|
|
26
28
|
recordIds: 'json|required', // Array of record IDs containing this word
|
|
27
29
|
count: 'number|required',
|
|
28
30
|
lastUpdated: 'string|required'
|
|
29
|
-
}
|
|
31
|
+
},
|
|
32
|
+
partitions: {
|
|
33
|
+
byResource: { fields: { resourceName: 'string' } }
|
|
34
|
+
},
|
|
35
|
+
behavior: 'body-overflow'
|
|
30
36
|
}));
|
|
31
37
|
this.indexResource = ok ? indexResource : this.database.resources.fulltext_indexes;
|
|
32
38
|
|
|
@@ -69,26 +75,71 @@ export class FullTextPlugin extends Plugin {
|
|
|
69
75
|
|
|
70
76
|
async saveIndexes() {
|
|
71
77
|
if (!this.indexResource) return;
|
|
72
|
-
|
|
78
|
+
|
|
73
79
|
const [ok, err] = await tryFn(async () => {
|
|
74
|
-
//
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
// Delete indexes that were removed
|
|
81
|
+
for (const key of this.deletedIndexes) {
|
|
82
|
+
// Find and delete the index record using partition-aware query
|
|
83
|
+
const [resourceName] = key.split(':');
|
|
84
|
+
const [queryOk, queryErr, results] = await tryFn(() =>
|
|
85
|
+
this.indexResource.query({ resourceName })
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (queryOk && results) {
|
|
89
|
+
for (const index of results) {
|
|
90
|
+
const indexKey = `${index.resourceName}:${index.fieldName}:${index.word}`;
|
|
91
|
+
if (indexKey === key) {
|
|
92
|
+
await this.indexResource.delete(index.id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
78
96
|
}
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
|
|
98
|
+
// Save or update dirty indexes
|
|
99
|
+
for (const key of this.dirtyIndexes) {
|
|
81
100
|
const [resourceName, fieldName, word] = key.split(':');
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
const data = this.indexes.get(key);
|
|
102
|
+
|
|
103
|
+
if (!data) continue; // Skip if index was deleted
|
|
104
|
+
|
|
105
|
+
// Try to find existing index record
|
|
106
|
+
const [queryOk, queryErr, results] = await tryFn(() =>
|
|
107
|
+
this.indexResource.query({ resourceName })
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
let existingRecord = null;
|
|
111
|
+
if (queryOk && results) {
|
|
112
|
+
existingRecord = results.find(
|
|
113
|
+
(index) => index.resourceName === resourceName &&
|
|
114
|
+
index.fieldName === fieldName &&
|
|
115
|
+
index.word === word
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (existingRecord) {
|
|
120
|
+
// Update existing record
|
|
121
|
+
await this.indexResource.update(existingRecord.id, {
|
|
122
|
+
recordIds: data.recordIds,
|
|
123
|
+
count: data.count,
|
|
124
|
+
lastUpdated: new Date().toISOString()
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
// Insert new record
|
|
128
|
+
await this.indexResource.insert({
|
|
129
|
+
id: `index-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
130
|
+
resourceName,
|
|
131
|
+
fieldName,
|
|
132
|
+
word,
|
|
133
|
+
recordIds: data.recordIds,
|
|
134
|
+
count: data.count,
|
|
135
|
+
lastUpdated: new Date().toISOString()
|
|
136
|
+
});
|
|
137
|
+
}
|
|
91
138
|
}
|
|
139
|
+
|
|
140
|
+
// Clear tracking sets after successful save
|
|
141
|
+
this.dirtyIndexes.clear();
|
|
142
|
+
this.deletedIndexes.clear();
|
|
92
143
|
});
|
|
93
144
|
}
|
|
94
145
|
|
|
@@ -195,21 +246,22 @@ export class FullTextPlugin extends Plugin {
|
|
|
195
246
|
}
|
|
196
247
|
|
|
197
248
|
const words = this.tokenize(fieldValue);
|
|
198
|
-
|
|
249
|
+
|
|
199
250
|
for (const word of words) {
|
|
200
251
|
if (word.length < this.config.minWordLength) {
|
|
201
252
|
continue;
|
|
202
253
|
}
|
|
203
|
-
|
|
254
|
+
|
|
204
255
|
const key = `${resourceName}:${fieldName}:${word.toLowerCase()}`;
|
|
205
256
|
const existing = this.indexes.get(key) || { recordIds: [], count: 0 };
|
|
206
|
-
|
|
257
|
+
|
|
207
258
|
if (!existing.recordIds.includes(recordId)) {
|
|
208
259
|
existing.recordIds.push(recordId);
|
|
209
260
|
existing.count = existing.recordIds.length;
|
|
210
261
|
}
|
|
211
|
-
|
|
262
|
+
|
|
212
263
|
this.indexes.set(key, existing);
|
|
264
|
+
this.dirtyIndexes.add(key); // Mark as dirty for incremental save
|
|
213
265
|
}
|
|
214
266
|
}
|
|
215
267
|
}
|
|
@@ -221,11 +273,13 @@ export class FullTextPlugin extends Plugin {
|
|
|
221
273
|
if (index > -1) {
|
|
222
274
|
data.recordIds.splice(index, 1);
|
|
223
275
|
data.count = data.recordIds.length;
|
|
224
|
-
|
|
276
|
+
|
|
225
277
|
if (data.recordIds.length === 0) {
|
|
226
278
|
this.indexes.delete(key);
|
|
279
|
+
this.deletedIndexes.add(key); // Track deletion for incremental save
|
|
227
280
|
} else {
|
|
228
281
|
this.indexes.set(key, data);
|
|
282
|
+
this.dirtyIndexes.add(key); // Mark as dirty for incremental save
|
|
229
283
|
}
|
|
230
284
|
}
|
|
231
285
|
}
|
|
@@ -47,8 +47,13 @@ export class MetricsPlugin extends Plugin {
|
|
|
47
47
|
errors: 'number|required',
|
|
48
48
|
avgTime: 'number|required',
|
|
49
49
|
timestamp: 'string|required',
|
|
50
|
-
metadata: 'json'
|
|
51
|
-
|
|
50
|
+
metadata: 'json',
|
|
51
|
+
createdAt: 'string|required' // YYYY-MM-DD for partitioning
|
|
52
|
+
},
|
|
53
|
+
partitions: {
|
|
54
|
+
byDate: { fields: { createdAt: 'string|maxlength:10' } }
|
|
55
|
+
},
|
|
56
|
+
behavior: 'body-overflow'
|
|
52
57
|
}));
|
|
53
58
|
this.metricsResource = ok1 ? metricsResource : this.database.resources.plg_metrics;
|
|
54
59
|
|
|
@@ -60,8 +65,13 @@ export class MetricsPlugin extends Plugin {
|
|
|
60
65
|
operation: 'string|required',
|
|
61
66
|
error: 'string|required',
|
|
62
67
|
timestamp: 'string|required',
|
|
63
|
-
metadata: 'json'
|
|
64
|
-
|
|
68
|
+
metadata: 'json',
|
|
69
|
+
createdAt: 'string|required' // YYYY-MM-DD for partitioning
|
|
70
|
+
},
|
|
71
|
+
partitions: {
|
|
72
|
+
byDate: { fields: { createdAt: 'string|maxlength:10' } }
|
|
73
|
+
},
|
|
74
|
+
behavior: 'body-overflow'
|
|
65
75
|
}));
|
|
66
76
|
this.errorsResource = ok2 ? errorsResource : this.database.resources.plg_error_logs;
|
|
67
77
|
|
|
@@ -73,8 +83,13 @@ export class MetricsPlugin extends Plugin {
|
|
|
73
83
|
operation: 'string|required',
|
|
74
84
|
duration: 'number|required',
|
|
75
85
|
timestamp: 'string|required',
|
|
76
|
-
metadata: 'json'
|
|
77
|
-
|
|
86
|
+
metadata: 'json',
|
|
87
|
+
createdAt: 'string|required' // YYYY-MM-DD for partitioning
|
|
88
|
+
},
|
|
89
|
+
partitions: {
|
|
90
|
+
byDate: { fields: { createdAt: 'string|maxlength:10' } }
|
|
91
|
+
},
|
|
92
|
+
behavior: 'body-overflow'
|
|
78
93
|
}));
|
|
79
94
|
this.performanceResource = ok3 ? performanceResource : this.database.resources.plg_performance_logs;
|
|
80
95
|
});
|
|
@@ -359,6 +374,9 @@ export class MetricsPlugin extends Plugin {
|
|
|
359
374
|
}
|
|
360
375
|
|
|
361
376
|
// Flush operation metrics
|
|
377
|
+
const now = new Date();
|
|
378
|
+
const createdAt = now.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
379
|
+
|
|
362
380
|
for (const [operation, data] of Object.entries(this.metrics.operations)) {
|
|
363
381
|
if (data.count > 0) {
|
|
364
382
|
await this.metricsResource.insert({
|
|
@@ -370,7 +388,8 @@ export class MetricsPlugin extends Plugin {
|
|
|
370
388
|
totalTime: data.totalTime,
|
|
371
389
|
errors: data.errors,
|
|
372
390
|
avgTime: data.count > 0 ? data.totalTime / data.count : 0,
|
|
373
|
-
timestamp:
|
|
391
|
+
timestamp: now.toISOString(),
|
|
392
|
+
createdAt,
|
|
374
393
|
metadata
|
|
375
394
|
});
|
|
376
395
|
}
|
|
@@ -389,7 +408,8 @@ export class MetricsPlugin extends Plugin {
|
|
|
389
408
|
totalTime: data.totalTime,
|
|
390
409
|
errors: data.errors,
|
|
391
410
|
avgTime: data.count > 0 ? data.totalTime / data.count : 0,
|
|
392
|
-
timestamp:
|
|
411
|
+
timestamp: now.toISOString(),
|
|
412
|
+
createdAt,
|
|
393
413
|
metadata: resourceMetadata
|
|
394
414
|
});
|
|
395
415
|
}
|
|
@@ -405,6 +425,7 @@ export class MetricsPlugin extends Plugin {
|
|
|
405
425
|
operation: perf.operation,
|
|
406
426
|
duration: perf.duration,
|
|
407
427
|
timestamp: perf.timestamp,
|
|
428
|
+
createdAt: perf.timestamp.slice(0, 10), // YYYY-MM-DD from timestamp
|
|
408
429
|
metadata: perfMetadata
|
|
409
430
|
});
|
|
410
431
|
}
|
|
@@ -420,6 +441,7 @@ export class MetricsPlugin extends Plugin {
|
|
|
420
441
|
error: error.error,
|
|
421
442
|
stack: error.stack,
|
|
422
443
|
timestamp: error.timestamp,
|
|
444
|
+
createdAt: error.timestamp.slice(0, 10), // YYYY-MM-DD from timestamp
|
|
423
445
|
metadata: errorMetadata
|
|
424
446
|
});
|
|
425
447
|
}
|
|
@@ -597,28 +619,56 @@ export class MetricsPlugin extends Plugin {
|
|
|
597
619
|
async cleanupOldData() {
|
|
598
620
|
const cutoffDate = new Date();
|
|
599
621
|
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
|
|
622
|
+
const cutoffDateStr = cutoffDate.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
623
|
+
|
|
624
|
+
// Generate list of dates to delete (all dates before cutoff)
|
|
625
|
+
const datesToDelete = [];
|
|
626
|
+
const startDate = new Date(cutoffDate);
|
|
627
|
+
startDate.setDate(startDate.getDate() - 365); // Go back up to 1 year to catch old data
|
|
628
|
+
|
|
629
|
+
for (let d = new Date(startDate); d < cutoffDate; d.setDate(d.getDate() + 1)) {
|
|
630
|
+
datesToDelete.push(d.toISOString().slice(0, 10));
|
|
631
|
+
}
|
|
600
632
|
|
|
601
|
-
// Clean up old metrics
|
|
633
|
+
// Clean up old metrics using partition-aware deletion
|
|
602
634
|
if (this.metricsResource) {
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
635
|
+
for (const dateStr of datesToDelete) {
|
|
636
|
+
const [ok, err, oldMetrics] = await tryFn(() =>
|
|
637
|
+
this.metricsResource.query({ createdAt: dateStr })
|
|
638
|
+
);
|
|
639
|
+
if (ok && oldMetrics) {
|
|
640
|
+
for (const metric of oldMetrics) {
|
|
641
|
+
await tryFn(() => this.metricsResource.delete(metric.id));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
606
644
|
}
|
|
607
645
|
}
|
|
608
646
|
|
|
609
|
-
// Clean up old error logs
|
|
647
|
+
// Clean up old error logs using partition-aware deletion
|
|
610
648
|
if (this.errorsResource) {
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
649
|
+
for (const dateStr of datesToDelete) {
|
|
650
|
+
const [ok, err, oldErrors] = await tryFn(() =>
|
|
651
|
+
this.errorsResource.query({ createdAt: dateStr })
|
|
652
|
+
);
|
|
653
|
+
if (ok && oldErrors) {
|
|
654
|
+
for (const error of oldErrors) {
|
|
655
|
+
await tryFn(() => this.errorsResource.delete(error.id));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
614
658
|
}
|
|
615
659
|
}
|
|
616
660
|
|
|
617
|
-
// Clean up old performance logs
|
|
661
|
+
// Clean up old performance logs using partition-aware deletion
|
|
618
662
|
if (this.performanceResource) {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
663
|
+
for (const dateStr of datesToDelete) {
|
|
664
|
+
const [ok, err, oldPerformance] = await tryFn(() =>
|
|
665
|
+
this.performanceResource.query({ createdAt: dateStr })
|
|
666
|
+
);
|
|
667
|
+
if (ok && oldPerformance) {
|
|
668
|
+
for (const perf of oldPerformance) {
|
|
669
|
+
await tryFn(() => this.performanceResource.delete(perf.id));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
622
672
|
}
|
|
623
673
|
}
|
|
624
674
|
}
|