s3db.js 7.4.2 → 8.0.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/PLUGINS.md +3 -3
- package/dist/s3db.cjs.js +516 -349
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +10 -0
- package/dist/s3db.es.js +516 -349
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +517 -351
- package/dist/s3db.iife.min.js +1 -1
- package/mcp/README.md +1062 -0
- package/mcp/server.js +1 -1
- package/package.json +31 -29
- package/src/client.class.js +26 -5
- package/src/database.class.js +151 -0
- package/src/plugins/audit.plugin.js +143 -310
- package/src/plugins/cache/filesystem-cache.class.js +40 -11
- package/src/plugins/cache.plugin.js +95 -47
- package/src/plugins/fulltext.plugin.js +21 -0
- package/src/plugins/metrics.plugin.js +21 -6
- package/src/plugins/replicator.plugin.js +65 -70
- package/src/resource.class.js +3 -2
- package/src/s3db.d.ts +10 -0
|
@@ -28,12 +28,12 @@ export class CachePlugin extends Plugin {
|
|
|
28
28
|
|
|
29
29
|
async onSetup() {
|
|
30
30
|
// Initialize cache driver
|
|
31
|
-
if (this.config.driver) {
|
|
32
|
-
// Use custom driver if provided
|
|
31
|
+
if (this.config.driver && typeof this.config.driver === 'object') {
|
|
32
|
+
// Use custom driver instance if provided
|
|
33
33
|
this.driver = this.config.driver;
|
|
34
|
-
} else if (this.config.
|
|
34
|
+
} else if (this.config.driver === 'memory') {
|
|
35
35
|
this.driver = new MemoryCache(this.config.memoryOptions || {});
|
|
36
|
-
} else if (this.config.
|
|
36
|
+
} else if (this.config.driver === 'filesystem') {
|
|
37
37
|
// Use partition-aware filesystem cache if enabled
|
|
38
38
|
if (this.config.partitionAware) {
|
|
39
39
|
this.driver = new PartitionAwareFilesystemCache({
|
|
@@ -50,13 +50,23 @@ export class CachePlugin extends Plugin {
|
|
|
50
50
|
this.driver = new S3Cache({ client: this.database.client, ...(this.config.s3Options || {}) });
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
this.
|
|
53
|
+
// Use database hooks instead of method overwriting
|
|
54
|
+
this.installDatabaseHooks();
|
|
55
55
|
|
|
56
56
|
// Install hooks for existing resources
|
|
57
57
|
this.installResourceHooks();
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Install database hooks to handle resource creation/updates
|
|
62
|
+
*/
|
|
63
|
+
installDatabaseHooks() {
|
|
64
|
+
// Hook into resource creation to install cache middleware
|
|
65
|
+
this.database.addHook('afterCreateResource', async ({ resource }) => {
|
|
66
|
+
this.installResourceHooksForResource(resource);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
async onStart() {
|
|
61
71
|
// Plugin is ready
|
|
62
72
|
}
|
|
@@ -65,27 +75,7 @@ export class CachePlugin extends Plugin {
|
|
|
65
75
|
// Cleanup if needed
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
installDatabaseProxy
|
|
69
|
-
if (this.database._cacheProxyInstalled) {
|
|
70
|
-
return; // Already installed
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const installResourceHooks = this.installResourceHooks.bind(this);
|
|
74
|
-
|
|
75
|
-
// Store original method
|
|
76
|
-
this.database._originalCreateResourceForCache = this.database.createResource;
|
|
77
|
-
|
|
78
|
-
// Create new method that doesn't call itself
|
|
79
|
-
this.database.createResource = async function (...args) {
|
|
80
|
-
const resource = await this._originalCreateResourceForCache(...args);
|
|
81
|
-
installResourceHooks(resource);
|
|
82
|
-
return resource;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// Mark as installed
|
|
86
|
-
this.database._cacheProxyInstalled = true;
|
|
87
|
-
}
|
|
88
|
-
|
|
78
|
+
// Remove the old installDatabaseProxy method
|
|
89
79
|
installResourceHooks() {
|
|
90
80
|
for (const resource of Object.values(this.database.resources)) {
|
|
91
81
|
this.installResourceHooksForResource(resource);
|
|
@@ -126,10 +116,12 @@ export class CachePlugin extends Plugin {
|
|
|
126
116
|
};
|
|
127
117
|
}
|
|
128
118
|
|
|
129
|
-
//
|
|
119
|
+
// Expanded list of methods to cache (including previously missing ones)
|
|
130
120
|
const cacheMethods = [
|
|
131
|
-
'count', 'listIds', 'getMany', 'getAll', 'page', 'list', 'get'
|
|
121
|
+
'count', 'listIds', 'getMany', 'getAll', 'page', 'list', 'get',
|
|
122
|
+
'exists', 'content', 'hasContent', 'query', 'getFromPartition'
|
|
132
123
|
];
|
|
124
|
+
|
|
133
125
|
for (const method of cacheMethods) {
|
|
134
126
|
resource.useMiddleware(method, async (ctx, next) => {
|
|
135
127
|
// Build cache key
|
|
@@ -142,11 +134,29 @@ export class CachePlugin extends Plugin {
|
|
|
142
134
|
} else if (method === 'list' || method === 'listIds' || method === 'count') {
|
|
143
135
|
const { partition, partitionValues } = ctx.args[0] || {};
|
|
144
136
|
key = await resource.cacheKeyFor({ action: method, partition, partitionValues });
|
|
137
|
+
} else if (method === 'query') {
|
|
138
|
+
const filter = ctx.args[0] || {};
|
|
139
|
+
const options = ctx.args[1] || {};
|
|
140
|
+
key = await resource.cacheKeyFor({
|
|
141
|
+
action: method,
|
|
142
|
+
params: { filter, options: { limit: options.limit, offset: options.offset } },
|
|
143
|
+
partition: options.partition,
|
|
144
|
+
partitionValues: options.partitionValues
|
|
145
|
+
});
|
|
146
|
+
} else if (method === 'getFromPartition') {
|
|
147
|
+
const { id, partitionName, partitionValues } = ctx.args[0] || {};
|
|
148
|
+
key = await resource.cacheKeyFor({
|
|
149
|
+
action: method,
|
|
150
|
+
params: { id, partitionName },
|
|
151
|
+
partition: partitionName,
|
|
152
|
+
partitionValues
|
|
153
|
+
});
|
|
145
154
|
} else if (method === 'getAll') {
|
|
146
155
|
key = await resource.cacheKeyFor({ action: method });
|
|
147
|
-
} else if (
|
|
156
|
+
} else if (['get', 'exists', 'content', 'hasContent'].includes(method)) {
|
|
148
157
|
key = await resource.cacheKeyFor({ action: method, params: { id: ctx.args[0] } });
|
|
149
158
|
}
|
|
159
|
+
|
|
150
160
|
// Try cache with partition awareness
|
|
151
161
|
let cached;
|
|
152
162
|
if (this.driver instanceof PartitionAwareFilesystemCache) {
|
|
@@ -156,6 +166,14 @@ export class CachePlugin extends Plugin {
|
|
|
156
166
|
const args = ctx.args[0] || {};
|
|
157
167
|
partition = args.partition;
|
|
158
168
|
partitionValues = args.partitionValues;
|
|
169
|
+
} else if (method === 'query') {
|
|
170
|
+
const options = ctx.args[1] || {};
|
|
171
|
+
partition = options.partition;
|
|
172
|
+
partitionValues = options.partitionValues;
|
|
173
|
+
} else if (method === 'getFromPartition') {
|
|
174
|
+
const { partitionName, partitionValues: pValues } = ctx.args[0] || {};
|
|
175
|
+
partition = partitionName;
|
|
176
|
+
partitionValues = pValues;
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
const [ok, err, result] = await tryFn(() => resource.cache._get(key, {
|
|
@@ -194,8 +212,8 @@ export class CachePlugin extends Plugin {
|
|
|
194
212
|
});
|
|
195
213
|
}
|
|
196
214
|
|
|
197
|
-
// List of methods to clear cache on write
|
|
198
|
-
const writeMethods = ['insert', 'update', 'delete', 'deleteMany'];
|
|
215
|
+
// List of methods to clear cache on write (expanded to include new methods)
|
|
216
|
+
const writeMethods = ['insert', 'update', 'delete', 'deleteMany', 'setContent', 'deleteContent', 'replace'];
|
|
199
217
|
for (const method of writeMethods) {
|
|
200
218
|
resource.useMiddleware(method, async (ctx, next) => {
|
|
201
219
|
const result = await next();
|
|
@@ -211,6 +229,12 @@ export class CachePlugin extends Plugin {
|
|
|
211
229
|
if (ok && full) data = full;
|
|
212
230
|
}
|
|
213
231
|
await this.clearCacheForResource(resource, data);
|
|
232
|
+
} else if (method === 'setContent' || method === 'deleteContent') {
|
|
233
|
+
const id = ctx.args[0]?.id || ctx.args[0];
|
|
234
|
+
await this.clearCacheForResource(resource, { id });
|
|
235
|
+
} else if (method === 'replace') {
|
|
236
|
+
const id = ctx.args[0];
|
|
237
|
+
await this.clearCacheForResource(resource, { id, ...ctx.args[1] });
|
|
214
238
|
} else if (method === 'deleteMany') {
|
|
215
239
|
// After all deletions, clear all aggregate and partition caches
|
|
216
240
|
await this.clearCacheForResource(resource);
|
|
@@ -225,28 +249,52 @@ export class CachePlugin extends Plugin {
|
|
|
225
249
|
|
|
226
250
|
const keyPrefix = `resource=${resource.name}`;
|
|
227
251
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
// For specific operations, only clear relevant cache entries
|
|
253
|
+
if (data && data.id) {
|
|
254
|
+
// Clear specific item caches for this ID
|
|
255
|
+
const itemSpecificMethods = ['get', 'exists', 'content', 'hasContent'];
|
|
256
|
+
for (const method of itemSpecificMethods) {
|
|
257
|
+
try {
|
|
258
|
+
const specificKey = await this.generateCacheKey(resource, method, { id: data.id });
|
|
259
|
+
await resource.cache.clear(specificKey.replace('.json.gz', ''));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
// Ignore cache clearing errors for individual items
|
|
238
262
|
}
|
|
239
|
-
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Clear partition-specific caches if this resource has partitions
|
|
266
|
+
if (this.config.includePartitions === true && resource.config?.partitions && Object.keys(resource.config.partitions).length > 0) {
|
|
240
267
|
const partitionValues = this.getPartitionValues(data, resource);
|
|
241
268
|
for (const [partitionName, values] of Object.entries(partitionValues)) {
|
|
242
|
-
// Only clear partition cache if there are actual values
|
|
243
269
|
if (values && Object.keys(values).length > 0 && Object.values(values).some(v => v !== null && v !== undefined)) {
|
|
244
|
-
|
|
245
|
-
|
|
270
|
+
try {
|
|
271
|
+
const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
|
|
272
|
+
await resource.cache.clear(partitionKeyPrefix);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// Ignore partition cache clearing errors
|
|
275
|
+
}
|
|
246
276
|
}
|
|
247
277
|
}
|
|
248
278
|
}
|
|
249
279
|
}
|
|
280
|
+
|
|
281
|
+
// Clear aggregate caches more broadly to ensure all variants are cleared
|
|
282
|
+
try {
|
|
283
|
+
// Clear all cache entries for this resource - this ensures aggregate methods are invalidated
|
|
284
|
+
await resource.cache.clear(keyPrefix);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
// If broad clearing fails, try specific method clearing
|
|
287
|
+
const aggregateMethods = ['count', 'list', 'listIds', 'getAll', 'page', 'query'];
|
|
288
|
+
for (const method of aggregateMethods) {
|
|
289
|
+
try {
|
|
290
|
+
// Try multiple key patterns to ensure we catch all variations
|
|
291
|
+
await resource.cache.clear(`${keyPrefix}/action=${method}`);
|
|
292
|
+
await resource.cache.clear(`resource=${resource.name}/action=${method}`);
|
|
293
|
+
} catch (methodError) {
|
|
294
|
+
// Ignore individual method clearing errors
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
250
298
|
}
|
|
251
299
|
|
|
252
300
|
async generateCacheKey(resource, action, params = {}, partition = null, partitionValues = null) {
|
|
@@ -277,7 +325,7 @@ export class CachePlugin extends Plugin {
|
|
|
277
325
|
async hashParams(params) {
|
|
278
326
|
const sortedParams = Object.keys(params)
|
|
279
327
|
.sort()
|
|
280
|
-
.map(key => `${key}:${params[key]}`)
|
|
328
|
+
.map(key => `${key}:${JSON.stringify(params[key])}`) // Use JSON.stringify for complex objects
|
|
281
329
|
.join('|') || 'empty';
|
|
282
330
|
|
|
283
331
|
return await sha256(sortedParams);
|
|
@@ -34,6 +34,10 @@ export class FullTextPlugin extends Plugin {
|
|
|
34
34
|
// Load existing indexes
|
|
35
35
|
await this.loadIndexes();
|
|
36
36
|
|
|
37
|
+
// Use database hooks for automatic resource discovery
|
|
38
|
+
this.installDatabaseHooks();
|
|
39
|
+
|
|
40
|
+
// Install hooks for existing resources
|
|
37
41
|
this.installIndexingHooks();
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -44,6 +48,9 @@ export class FullTextPlugin extends Plugin {
|
|
|
44
48
|
async stop() {
|
|
45
49
|
// Save indexes before stopping
|
|
46
50
|
await this.saveIndexes();
|
|
51
|
+
|
|
52
|
+
// Remove database hooks
|
|
53
|
+
this.removeDatabaseHooks();
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
async loadIndexes() {
|
|
@@ -86,6 +93,20 @@ export class FullTextPlugin extends Plugin {
|
|
|
86
93
|
});
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
installDatabaseHooks() {
|
|
97
|
+
// Use the new database hooks system for automatic resource discovery
|
|
98
|
+
this.database.addHook('afterCreateResource', (resource) => {
|
|
99
|
+
if (resource.name !== 'fulltext_indexes') {
|
|
100
|
+
this.installResourceHooks(resource);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
removeDatabaseHooks() {
|
|
106
|
+
// Remove the hook we added
|
|
107
|
+
this.database.removeHook('afterCreateResource', this.installResourceHooks.bind(this));
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
installIndexingHooks() {
|
|
90
111
|
// Register plugin with database
|
|
91
112
|
if (!this.database.plugins) {
|
|
@@ -86,7 +86,10 @@ export class MetricsPlugin extends Plugin {
|
|
|
86
86
|
this.performanceResource = database.resources.performance_logs;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
//
|
|
89
|
+
// Use database hooks for automatic resource discovery
|
|
90
|
+
this.installDatabaseHooks();
|
|
91
|
+
|
|
92
|
+
// Install hooks for existing resources
|
|
90
93
|
this.installMetricsHooks();
|
|
91
94
|
|
|
92
95
|
// Disable flush timer during tests to avoid side effects
|
|
@@ -100,16 +103,28 @@ export class MetricsPlugin extends Plugin {
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
async stop() {
|
|
103
|
-
// Stop flush timer
|
|
106
|
+
// Stop flush timer
|
|
104
107
|
if (this.flushTimer) {
|
|
105
108
|
clearInterval(this.flushTimer);
|
|
106
109
|
this.flushTimer = null;
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
// Remove database hooks
|
|
113
|
+
this.removeDatabaseHooks();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
installDatabaseHooks() {
|
|
117
|
+
// Use the new database hooks system for automatic resource discovery
|
|
118
|
+
this.database.addHook('afterCreateResource', (resource) => {
|
|
119
|
+
if (resource.name !== 'metrics' && resource.name !== 'error_logs' && resource.name !== 'performance_logs') {
|
|
120
|
+
this.installResourceHooks(resource);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
removeDatabaseHooks() {
|
|
126
|
+
// Remove the hook we added
|
|
127
|
+
this.database.removeHook('afterCreateResource', this.installResourceHooks.bind(this));
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
installMetricsHooks() {
|
|
@@ -164,6 +164,13 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
164
164
|
return filtered;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
async getCompleteData(resource, data) {
|
|
168
|
+
// Always get the complete record from the resource to ensure we have all data
|
|
169
|
+
// This handles all behaviors: body-overflow, truncate-data, body-only, etc.
|
|
170
|
+
const [ok, err, completeRecord] = await tryFn(() => resource.get(data.id));
|
|
171
|
+
return ok ? completeRecord : data;
|
|
172
|
+
}
|
|
173
|
+
|
|
167
174
|
installEventListeners(resource, database, plugin) {
|
|
168
175
|
if (!resource || this.eventListenersInstalled.has(resource.name) ||
|
|
169
176
|
resource.name === this.config.replicatorLogResource) {
|
|
@@ -186,8 +193,10 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
186
193
|
|
|
187
194
|
resource.on('update', async (data, beforeData) => {
|
|
188
195
|
const [ok, error] = await tryFn(async () => {
|
|
189
|
-
|
|
190
|
-
await plugin.
|
|
196
|
+
// For updates, we need to get the complete updated record, not just the changed fields
|
|
197
|
+
const completeData = await plugin.getCompleteData(resource, data);
|
|
198
|
+
const dataWithTimestamp = { ...completeData, updatedAt: new Date().toISOString() };
|
|
199
|
+
await plugin.processReplicatorEvent('update', resource.name, completeData.id, dataWithTimestamp, beforeData);
|
|
191
200
|
});
|
|
192
201
|
|
|
193
202
|
if (!ok) {
|
|
@@ -214,76 +223,73 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
214
223
|
this.eventListenersInstalled.add(resource.name);
|
|
215
224
|
}
|
|
216
225
|
|
|
217
|
-
/**
|
|
218
|
-
* Get complete data by always fetching the full record from the resource
|
|
219
|
-
* This ensures we always have the complete data regardless of behavior or data size
|
|
220
|
-
*/
|
|
221
|
-
async getCompleteData(resource, data) {
|
|
222
|
-
// Always get the complete record from the resource to ensure we have all data
|
|
223
|
-
// This handles all behaviors: body-overflow, truncate-data, body-only, etc.
|
|
224
|
-
const [ok, err, completeRecord] = await tryFn(() => resource.get(data.id));
|
|
225
|
-
return ok ? completeRecord : data;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
226
|
async setup(database) {
|
|
229
227
|
this.database = database;
|
|
230
|
-
|
|
231
|
-
const [initOk, initError] = await tryFn(async () => {
|
|
232
|
-
await this.initializeReplicators(database);
|
|
233
|
-
});
|
|
234
228
|
|
|
235
|
-
if
|
|
236
|
-
|
|
237
|
-
|
|
229
|
+
// Create replicator log resource if enabled
|
|
230
|
+
if (this.config.persistReplicatorLog) {
|
|
231
|
+
const [ok, err, logResource] = await tryFn(() => database.createResource({
|
|
232
|
+
name: this.config.replicatorLogResource || 'replicator_logs',
|
|
233
|
+
attributes: {
|
|
234
|
+
id: 'string|required',
|
|
235
|
+
resource: 'string|required',
|
|
236
|
+
action: 'string|required',
|
|
237
|
+
data: 'json',
|
|
238
|
+
timestamp: 'number|required',
|
|
239
|
+
createdAt: 'string|required'
|
|
240
|
+
},
|
|
241
|
+
behavior: 'truncate-data'
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
if (ok) {
|
|
245
|
+
this.replicatorLogResource = logResource;
|
|
246
|
+
} else {
|
|
247
|
+
this.replicatorLogResource = database.resources[this.config.replicatorLogResource || 'replicator_logs'];
|
|
238
248
|
}
|
|
239
|
-
this.emit('error', { operation: 'setup', error: initError.message });
|
|
240
|
-
throw initError;
|
|
241
249
|
}
|
|
242
250
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const logRes = await database.createResource({
|
|
246
|
-
name: this.config.replicatorLogResource,
|
|
247
|
-
behavior: 'body-overflow',
|
|
248
|
-
attributes: {
|
|
249
|
-
operation: 'string',
|
|
250
|
-
resourceName: 'string',
|
|
251
|
-
recordId: 'string',
|
|
252
|
-
data: 'string',
|
|
253
|
-
error: 'string|optional',
|
|
254
|
-
replicator: 'string',
|
|
255
|
-
timestamp: 'string',
|
|
256
|
-
status: 'string'
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
});
|
|
251
|
+
// Initialize replicators
|
|
252
|
+
await this.initializeReplicators(database);
|
|
261
253
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
254
|
+
// Use database hooks for automatic resource discovery
|
|
255
|
+
this.installDatabaseHooks();
|
|
256
|
+
|
|
257
|
+
// Install event listeners for existing resources
|
|
258
|
+
for (const resource of Object.values(database.resources)) {
|
|
259
|
+
if (resource.name !== (this.config.replicatorLogResource || 'replicator_logs')) {
|
|
260
|
+
this.installEventListeners(resource, database, this);
|
|
265
261
|
}
|
|
266
|
-
this.emit('replicator_log_resource_creation_error', {
|
|
267
|
-
resourceName: this.config.replicatorLogResource,
|
|
268
|
-
error: logError.message
|
|
269
|
-
});
|
|
270
262
|
}
|
|
263
|
+
}
|
|
271
264
|
|
|
272
|
-
|
|
265
|
+
async start() {
|
|
266
|
+
// Plugin is ready
|
|
267
|
+
}
|
|
273
268
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
269
|
+
async stop() {
|
|
270
|
+
// Stop all replicators
|
|
271
|
+
for (const replicator of this.replicators || []) {
|
|
272
|
+
if (replicator && typeof replicator.cleanup === 'function') {
|
|
273
|
+
await replicator.cleanup();
|
|
279
274
|
}
|
|
280
|
-
return resource;
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
for (const resourceName in database.resources) {
|
|
284
|
-
const resource = database.resources[resourceName];
|
|
285
|
-
this.installEventListeners(resource, database, this);
|
|
286
275
|
}
|
|
276
|
+
|
|
277
|
+
// Remove database hooks
|
|
278
|
+
this.removeDatabaseHooks();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
installDatabaseHooks() {
|
|
282
|
+
// Use the new database hooks system for automatic resource discovery
|
|
283
|
+
this.database.addHook('afterCreateResource', (resource) => {
|
|
284
|
+
if (resource.name !== (this.config.replicatorLogResource || 'replicator_logs')) {
|
|
285
|
+
this.installEventListeners(resource, this.database, this);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
removeDatabaseHooks() {
|
|
291
|
+
// Remove the hook we added
|
|
292
|
+
this.database.removeHook('afterCreateResource', this.installEventListeners.bind(this));
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
createReplicator(driver, config, resources, client) {
|
|
@@ -309,17 +315,6 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
309
315
|
}
|
|
310
316
|
}
|
|
311
317
|
|
|
312
|
-
async start() {
|
|
313
|
-
// Plugin is ready
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async stop() {
|
|
317
|
-
// Stop queue processing
|
|
318
|
-
// this.isProcessing = false; // Removed as per edit hint
|
|
319
|
-
// Process remaining queue items
|
|
320
|
-
// await this.processQueue(); // Removed as per edit hint
|
|
321
|
-
}
|
|
322
|
-
|
|
323
318
|
async uploadMetadataFile(database) {
|
|
324
319
|
if (typeof database.uploadMetadataFile === 'function') {
|
|
325
320
|
await database.uploadMetadataFile();
|
package/src/resource.class.js
CHANGED
|
@@ -2484,10 +2484,11 @@ export class Resource extends EventEmitter {
|
|
|
2484
2484
|
_initMiddleware() {
|
|
2485
2485
|
// Map of methodName -> array of middleware functions
|
|
2486
2486
|
this._middlewares = new Map();
|
|
2487
|
-
// Supported methods for middleware
|
|
2487
|
+
// Supported methods for middleware (expanded to include newly cached methods)
|
|
2488
2488
|
this._middlewareMethods = [
|
|
2489
2489
|
'get', 'list', 'listIds', 'getAll', 'count', 'page',
|
|
2490
|
-
'insert', 'update', 'delete', 'deleteMany', 'exists', 'getMany'
|
|
2490
|
+
'insert', 'update', 'delete', 'deleteMany', 'exists', 'getMany',
|
|
2491
|
+
'content', 'hasContent', 'query', 'getFromPartition', 'setContent', 'deleteContent', 'replace'
|
|
2491
2492
|
];
|
|
2492
2493
|
for (const method of this._middlewareMethods) {
|
|
2493
2494
|
this._middlewares.set(method, []);
|
package/src/s3db.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ declare module 's3db.js' {
|
|
|
6
6
|
// CORE TYPES
|
|
7
7
|
// ============================================================================
|
|
8
8
|
|
|
9
|
+
/** HTTP Client configuration for keep-alive and connection pooling */
|
|
10
|
+
export interface HttpClientOptions {
|
|
11
|
+
keepAlive?: boolean;
|
|
12
|
+
keepAliveMsecs?: number;
|
|
13
|
+
maxSockets?: number;
|
|
14
|
+
maxFreeSockets?: number;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/** Main Database configuration */
|
|
10
19
|
export interface DatabaseConfig {
|
|
11
20
|
connectionString?: string;
|
|
@@ -23,6 +32,7 @@ declare module 's3db.js' {
|
|
|
23
32
|
cache?: CacheConfig | boolean;
|
|
24
33
|
plugins?: (PluginInterface | PluginFunction)[];
|
|
25
34
|
client?: Client;
|
|
35
|
+
httpClientOptions?: HttpClientOptions;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
/** Resource configuration */
|