s3db.js 6.1.0 → 7.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 +2724 -0
- package/README.md +377 -492
- package/UNLICENSE +24 -0
- package/dist/s3db.cjs.js +30054 -18189
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +373 -72
- package/dist/s3db.es.js +30040 -18186
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +29727 -17863
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +44 -69
- package/src/behaviors/body-only.js +110 -0
- package/src/behaviors/body-overflow.js +153 -0
- package/src/behaviors/enforce-limits.js +195 -0
- package/src/behaviors/index.js +39 -0
- package/src/behaviors/truncate-data.js +204 -0
- package/src/behaviors/user-managed.js +147 -0
- package/src/client.class.js +515 -0
- package/src/concerns/base62.js +61 -0
- package/src/concerns/calculator.js +204 -0
- package/src/concerns/crypto.js +142 -0
- package/src/concerns/id.js +8 -0
- package/src/concerns/index.js +5 -0
- package/src/concerns/try-fn.js +151 -0
- package/src/connection-string.class.js +75 -0
- package/src/database.class.js +599 -0
- package/src/errors.js +261 -0
- package/src/index.js +17 -0
- package/src/plugins/audit.plugin.js +442 -0
- package/src/plugins/cache/cache.class.js +53 -0
- package/src/plugins/cache/index.js +6 -0
- package/src/plugins/cache/memory-cache.class.js +164 -0
- package/src/plugins/cache/s3-cache.class.js +189 -0
- package/src/plugins/cache.plugin.js +275 -0
- package/src/plugins/consumers/index.js +24 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
- package/src/plugins/consumers/sqs-consumer.js +102 -0
- package/src/plugins/costs.plugin.js +81 -0
- package/src/plugins/fulltext.plugin.js +473 -0
- package/src/plugins/index.js +12 -0
- package/src/plugins/metrics.plugin.js +603 -0
- package/src/plugins/plugin.class.js +210 -0
- package/src/plugins/plugin.obj.js +13 -0
- package/src/plugins/queue-consumer.plugin.js +134 -0
- package/src/plugins/replicator.plugin.js +769 -0
- package/src/plugins/replicators/base-replicator.class.js +85 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
- package/src/plugins/replicators/index.js +44 -0
- package/src/plugins/replicators/postgres-replicator.class.js +427 -0
- package/src/plugins/replicators/s3db-replicator.class.js +352 -0
- package/src/plugins/replicators/sqs-replicator.class.js +427 -0
- package/src/resource.class.js +2626 -0
- package/src/s3db.d.ts +1263 -0
- package/src/schema.class.js +706 -0
- package/src/stream/index.js +16 -0
- package/src/stream/resource-ids-page-reader.class.js +10 -0
- package/src/stream/resource-ids-reader.class.js +63 -0
- package/src/stream/resource-reader.class.js +81 -0
- package/src/stream/resource-writer.class.js +92 -0
- package/src/validator.class.js +97 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const CostsPlugin = {
|
|
2
|
+
async setup (db) {
|
|
3
|
+
if (!db || !db.client) {
|
|
4
|
+
return; // Handle null/invalid database gracefully
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
this.client = db.client
|
|
8
|
+
|
|
9
|
+
this.map = {
|
|
10
|
+
PutObjectCommand: 'put',
|
|
11
|
+
GetObjectCommand: 'get',
|
|
12
|
+
HeadObjectCommand: 'head',
|
|
13
|
+
DeleteObjectCommand: 'delete',
|
|
14
|
+
DeleteObjectsCommand: 'delete',
|
|
15
|
+
ListObjectsV2Command: 'list',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.costs = {
|
|
19
|
+
total: 0,
|
|
20
|
+
prices: {
|
|
21
|
+
put: 0.005 / 1000,
|
|
22
|
+
copy: 0.005 / 1000,
|
|
23
|
+
list: 0.005 / 1000,
|
|
24
|
+
post: 0.005 / 1000,
|
|
25
|
+
get: 0.0004 / 1000,
|
|
26
|
+
select: 0.0004 / 1000,
|
|
27
|
+
delete: 0.0004 / 1000,
|
|
28
|
+
head: 0.0004 / 1000,
|
|
29
|
+
},
|
|
30
|
+
requests: {
|
|
31
|
+
total: 0,
|
|
32
|
+
put: 0,
|
|
33
|
+
post: 0,
|
|
34
|
+
copy: 0,
|
|
35
|
+
list: 0,
|
|
36
|
+
get: 0,
|
|
37
|
+
select: 0,
|
|
38
|
+
delete: 0,
|
|
39
|
+
head: 0,
|
|
40
|
+
},
|
|
41
|
+
events: {
|
|
42
|
+
total: 0,
|
|
43
|
+
PutObjectCommand: 0,
|
|
44
|
+
GetObjectCommand: 0,
|
|
45
|
+
HeadObjectCommand: 0,
|
|
46
|
+
DeleteObjectCommand: 0,
|
|
47
|
+
DeleteObjectsCommand: 0,
|
|
48
|
+
ListObjectsV2Command: 0,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.client.costs = JSON.parse(JSON.stringify(this.costs));
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async start () {
|
|
56
|
+
if (this.client) {
|
|
57
|
+
this.client.on("command.response", (name) => this.addRequest(name, this.map[name]));
|
|
58
|
+
this.client.on("command.error", (name) => this.addRequest(name, this.map[name]));
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
addRequest (name, method) {
|
|
63
|
+
if (!method) return; // Skip if no mapping found
|
|
64
|
+
|
|
65
|
+
this.costs.events[name]++;
|
|
66
|
+
this.costs.events.total++;
|
|
67
|
+
this.costs.requests.total++;
|
|
68
|
+
this.costs.requests[method]++;
|
|
69
|
+
this.costs.total += this.costs.prices[method];
|
|
70
|
+
|
|
71
|
+
if (this.client && this.client.costs) {
|
|
72
|
+
this.client.costs.events[name]++;
|
|
73
|
+
this.client.costs.events.total++;
|
|
74
|
+
this.client.costs.requests.total++;
|
|
75
|
+
this.client.costs.requests[method]++;
|
|
76
|
+
this.client.costs.total += this.client.costs.prices[method];
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default CostsPlugin
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import Plugin from "./plugin.class.js";
|
|
2
|
+
import tryFn from "../concerns/try-fn.js";
|
|
3
|
+
|
|
4
|
+
export class FullTextPlugin extends Plugin {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
super();
|
|
7
|
+
this.indexResource = null;
|
|
8
|
+
this.config = {
|
|
9
|
+
minWordLength: options.minWordLength || 3,
|
|
10
|
+
maxResults: options.maxResults || 100,
|
|
11
|
+
...options
|
|
12
|
+
};
|
|
13
|
+
this.indexes = new Map(); // In-memory index for simplicity
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async setup(database) {
|
|
17
|
+
this.database = database;
|
|
18
|
+
|
|
19
|
+
// Create index resource if it doesn't exist
|
|
20
|
+
const [ok, err, indexResource] = await tryFn(() => database.createResource({
|
|
21
|
+
name: 'fulltext_indexes',
|
|
22
|
+
attributes: {
|
|
23
|
+
id: 'string|required',
|
|
24
|
+
resourceName: 'string|required',
|
|
25
|
+
fieldName: 'string|required',
|
|
26
|
+
word: 'string|required',
|
|
27
|
+
recordIds: 'json|required', // Array of record IDs containing this word
|
|
28
|
+
count: 'number|required',
|
|
29
|
+
lastUpdated: 'string|required'
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
this.indexResource = ok ? indexResource : database.resources.fulltext_indexes;
|
|
33
|
+
|
|
34
|
+
// Load existing indexes
|
|
35
|
+
await this.loadIndexes();
|
|
36
|
+
|
|
37
|
+
this.installIndexingHooks();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async start() {
|
|
41
|
+
// Plugin is ready
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async stop() {
|
|
45
|
+
// Save indexes before stopping
|
|
46
|
+
await this.saveIndexes();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async loadIndexes() {
|
|
50
|
+
if (!this.indexResource) return;
|
|
51
|
+
|
|
52
|
+
const [ok, err, allIndexes] = await tryFn(() => this.indexResource.getAll());
|
|
53
|
+
if (ok) {
|
|
54
|
+
for (const indexRecord of allIndexes) {
|
|
55
|
+
const key = `${indexRecord.resourceName}:${indexRecord.fieldName}:${indexRecord.word}`;
|
|
56
|
+
this.indexes.set(key, {
|
|
57
|
+
recordIds: indexRecord.recordIds || [],
|
|
58
|
+
count: indexRecord.count || 0
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async saveIndexes() {
|
|
65
|
+
if (!this.indexResource) return;
|
|
66
|
+
|
|
67
|
+
const [ok, err] = await tryFn(async () => {
|
|
68
|
+
// Clear existing indexes
|
|
69
|
+
const existingIndexes = await this.indexResource.getAll();
|
|
70
|
+
for (const index of existingIndexes) {
|
|
71
|
+
await this.indexResource.delete(index.id);
|
|
72
|
+
}
|
|
73
|
+
// Save current indexes
|
|
74
|
+
for (const [key, data] of this.indexes.entries()) {
|
|
75
|
+
const [resourceName, fieldName, word] = key.split(':');
|
|
76
|
+
await this.indexResource.insert({
|
|
77
|
+
id: `index-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
78
|
+
resourceName,
|
|
79
|
+
fieldName,
|
|
80
|
+
word,
|
|
81
|
+
recordIds: data.recordIds,
|
|
82
|
+
count: data.count,
|
|
83
|
+
lastUpdated: new Date().toISOString()
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
installIndexingHooks() {
|
|
90
|
+
// Register plugin with database
|
|
91
|
+
if (!this.database.plugins) {
|
|
92
|
+
this.database.plugins = {};
|
|
93
|
+
}
|
|
94
|
+
this.database.plugins.fulltext = this;
|
|
95
|
+
|
|
96
|
+
for (const resource of Object.values(this.database.resources)) {
|
|
97
|
+
if (resource.name === 'fulltext_indexes') continue;
|
|
98
|
+
|
|
99
|
+
this.installResourceHooks(resource);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Hook into database proxy for new resources (check if already installed)
|
|
103
|
+
if (!this.database._fulltextProxyInstalled) {
|
|
104
|
+
// Store the previous createResource (could be another plugin's proxy)
|
|
105
|
+
this.database._previousCreateResourceForFullText = this.database.createResource;
|
|
106
|
+
this.database.createResource = async function (...args) {
|
|
107
|
+
const resource = await this._previousCreateResourceForFullText(...args);
|
|
108
|
+
if (this.plugins?.fulltext && resource.name !== 'fulltext_indexes') {
|
|
109
|
+
this.plugins.fulltext.installResourceHooks(resource);
|
|
110
|
+
}
|
|
111
|
+
return resource;
|
|
112
|
+
};
|
|
113
|
+
this.database._fulltextProxyInstalled = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Ensure all existing resources have hooks (even if created before plugin setup)
|
|
117
|
+
for (const resource of Object.values(this.database.resources)) {
|
|
118
|
+
if (resource.name !== 'fulltext_indexes') {
|
|
119
|
+
this.installResourceHooks(resource);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
installResourceHooks(resource) {
|
|
125
|
+
// Store original methods
|
|
126
|
+
resource._insert = resource.insert;
|
|
127
|
+
resource._update = resource.update;
|
|
128
|
+
resource._delete = resource.delete;
|
|
129
|
+
resource._deleteMany = resource.deleteMany;
|
|
130
|
+
|
|
131
|
+
// Use wrapResourceMethod for all hooks so _pluginWrappers is set
|
|
132
|
+
this.wrapResourceMethod(resource, 'insert', async (result, args, methodName) => {
|
|
133
|
+
const [data] = args;
|
|
134
|
+
// Index the new record
|
|
135
|
+
this.indexRecord(resource.name, result.id, data).catch(console.error);
|
|
136
|
+
return result;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this.wrapResourceMethod(resource, 'update', async (result, args, methodName) => {
|
|
140
|
+
const [id, data] = args;
|
|
141
|
+
// Remove old index entries
|
|
142
|
+
this.removeRecordFromIndex(resource.name, id).catch(console.error);
|
|
143
|
+
// Index the updated record
|
|
144
|
+
this.indexRecord(resource.name, id, result).catch(console.error);
|
|
145
|
+
return result;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this.wrapResourceMethod(resource, 'delete', async (result, args, methodName) => {
|
|
149
|
+
const [id] = args;
|
|
150
|
+
// Remove from index
|
|
151
|
+
this.removeRecordFromIndex(resource.name, id).catch(console.error);
|
|
152
|
+
return result;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
this.wrapResourceMethod(resource, 'deleteMany', async (result, args, methodName) => {
|
|
156
|
+
const [ids] = args;
|
|
157
|
+
// Remove from index
|
|
158
|
+
for (const id of ids) {
|
|
159
|
+
this.removeRecordFromIndex(resource.name, id).catch(console.error);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async indexRecord(resourceName, recordId, data) {
|
|
166
|
+
const indexedFields = this.getIndexedFields(resourceName);
|
|
167
|
+
if (!indexedFields || indexedFields.length === 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const fieldName of indexedFields) {
|
|
172
|
+
const fieldValue = this.getFieldValue(data, fieldName);
|
|
173
|
+
if (!fieldValue) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const words = this.tokenize(fieldValue);
|
|
178
|
+
|
|
179
|
+
for (const word of words) {
|
|
180
|
+
if (word.length < this.config.minWordLength) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const key = `${resourceName}:${fieldName}:${word.toLowerCase()}`;
|
|
185
|
+
const existing = this.indexes.get(key) || { recordIds: [], count: 0 };
|
|
186
|
+
|
|
187
|
+
if (!existing.recordIds.includes(recordId)) {
|
|
188
|
+
existing.recordIds.push(recordId);
|
|
189
|
+
existing.count = existing.recordIds.length;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.indexes.set(key, existing);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async removeRecordFromIndex(resourceName, recordId) {
|
|
198
|
+
for (const [key, data] of this.indexes.entries()) {
|
|
199
|
+
if (key.startsWith(`${resourceName}:`)) {
|
|
200
|
+
const index = data.recordIds.indexOf(recordId);
|
|
201
|
+
if (index > -1) {
|
|
202
|
+
data.recordIds.splice(index, 1);
|
|
203
|
+
data.count = data.recordIds.length;
|
|
204
|
+
|
|
205
|
+
if (data.recordIds.length === 0) {
|
|
206
|
+
this.indexes.delete(key);
|
|
207
|
+
} else {
|
|
208
|
+
this.indexes.set(key, data);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
getFieldValue(data, fieldPath) {
|
|
216
|
+
if (!fieldPath.includes('.')) {
|
|
217
|
+
return data && data[fieldPath] !== undefined ? data[fieldPath] : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const keys = fieldPath.split('.');
|
|
221
|
+
let value = data;
|
|
222
|
+
|
|
223
|
+
for (const key of keys) {
|
|
224
|
+
if (value && typeof value === 'object' && key in value) {
|
|
225
|
+
value = value[key];
|
|
226
|
+
} else {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
tokenize(text) {
|
|
235
|
+
if (!text) return [];
|
|
236
|
+
|
|
237
|
+
// Convert to string and normalize
|
|
238
|
+
const str = String(text).toLowerCase();
|
|
239
|
+
|
|
240
|
+
// Remove special characters but preserve accented characters
|
|
241
|
+
return str
|
|
242
|
+
.replace(/[^\w\s\u00C0-\u017F]/g, ' ') // Allow accented characters
|
|
243
|
+
.split(/\s+/)
|
|
244
|
+
.filter(word => word.length > 0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getIndexedFields(resourceName) {
|
|
248
|
+
// Use configured fields if available, otherwise fall back to defaults
|
|
249
|
+
if (this.config.fields) {
|
|
250
|
+
return this.config.fields;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Default field mappings
|
|
254
|
+
const fieldMappings = {
|
|
255
|
+
users: ['name', 'email'],
|
|
256
|
+
products: ['name', 'description'],
|
|
257
|
+
articles: ['title', 'content'],
|
|
258
|
+
// Add more mappings as needed
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return fieldMappings[resourceName] || [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Main search method
|
|
265
|
+
async search(resourceName, query, options = {}) {
|
|
266
|
+
const {
|
|
267
|
+
fields = null, // Specific fields to search in
|
|
268
|
+
limit = this.config.maxResults,
|
|
269
|
+
offset = 0,
|
|
270
|
+
exactMatch = false
|
|
271
|
+
} = options;
|
|
272
|
+
|
|
273
|
+
if (!query || query.trim().length === 0) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const searchWords = this.tokenize(query);
|
|
278
|
+
const results = new Map(); // recordId -> score
|
|
279
|
+
|
|
280
|
+
// Get fields to search in
|
|
281
|
+
const searchFields = fields || this.getIndexedFields(resourceName);
|
|
282
|
+
if (searchFields.length === 0) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Search for each word
|
|
287
|
+
for (const word of searchWords) {
|
|
288
|
+
if (word.length < this.config.minWordLength) continue;
|
|
289
|
+
|
|
290
|
+
for (const fieldName of searchFields) {
|
|
291
|
+
if (exactMatch) {
|
|
292
|
+
// Exact match - look for the exact word
|
|
293
|
+
const key = `${resourceName}:${fieldName}:${word.toLowerCase()}`;
|
|
294
|
+
const indexData = this.indexes.get(key);
|
|
295
|
+
|
|
296
|
+
if (indexData) {
|
|
297
|
+
for (const recordId of indexData.recordIds) {
|
|
298
|
+
const currentScore = results.get(recordId) || 0;
|
|
299
|
+
results.set(recordId, currentScore + 1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
// Partial match - look for words that start with the search term
|
|
304
|
+
for (const [key, indexData] of this.indexes.entries()) {
|
|
305
|
+
if (key.startsWith(`${resourceName}:${fieldName}:${word.toLowerCase()}`)) {
|
|
306
|
+
for (const recordId of indexData.recordIds) {
|
|
307
|
+
const currentScore = results.get(recordId) || 0;
|
|
308
|
+
results.set(recordId, currentScore + 1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Convert to sorted results
|
|
317
|
+
const sortedResults = Array.from(results.entries())
|
|
318
|
+
.map(([recordId, score]) => ({ recordId, score }))
|
|
319
|
+
.sort((a, b) => b.score - a.score)
|
|
320
|
+
.slice(offset, offset + limit);
|
|
321
|
+
|
|
322
|
+
return sortedResults;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Search and return full records
|
|
326
|
+
async searchRecords(resourceName, query, options = {}) {
|
|
327
|
+
const searchResults = await this.search(resourceName, query, options);
|
|
328
|
+
|
|
329
|
+
if (searchResults.length === 0) {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const resource = this.database.resources[resourceName];
|
|
334
|
+
if (!resource) {
|
|
335
|
+
throw new Error(`Resource '${resourceName}' not found`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const recordIds = searchResults.map(result => result.recordId);
|
|
339
|
+
const records = await resource.getMany(recordIds);
|
|
340
|
+
|
|
341
|
+
// Filter out undefined/null records (in case getMany returns missing records)
|
|
342
|
+
const result = records
|
|
343
|
+
.filter(record => record && typeof record === 'object')
|
|
344
|
+
.map(record => {
|
|
345
|
+
const searchResult = searchResults.find(sr => sr.recordId === record.id);
|
|
346
|
+
return {
|
|
347
|
+
...record,
|
|
348
|
+
_searchScore: searchResult ? searchResult.score : 0
|
|
349
|
+
};
|
|
350
|
+
})
|
|
351
|
+
.sort((a, b) => b._searchScore - a._searchScore);
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Utility methods
|
|
356
|
+
async rebuildIndex(resourceName) {
|
|
357
|
+
const resource = this.database.resources[resourceName];
|
|
358
|
+
if (!resource) {
|
|
359
|
+
throw new Error(`Resource '${resourceName}' not found`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Clear existing indexes for this resource
|
|
363
|
+
for (const [key] of this.indexes.entries()) {
|
|
364
|
+
if (key.startsWith(`${resourceName}:`)) {
|
|
365
|
+
this.indexes.delete(key);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Rebuild index in larger batches for better performance
|
|
370
|
+
const allRecords = await resource.getAll();
|
|
371
|
+
const batchSize = 100; // Increased batch size for faster processing
|
|
372
|
+
|
|
373
|
+
for (let i = 0; i < allRecords.length; i += batchSize) {
|
|
374
|
+
const batch = allRecords.slice(i, i + batchSize);
|
|
375
|
+
// Process batch sequentially to avoid overwhelming the system
|
|
376
|
+
for (const record of batch) {
|
|
377
|
+
const [ok, err] = await tryFn(() => this.indexRecord(resourceName, record.id, record));
|
|
378
|
+
if (!ok) {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Save indexes
|
|
384
|
+
await this.saveIndexes();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async getIndexStats() {
|
|
388
|
+
const stats = {
|
|
389
|
+
totalIndexes: this.indexes.size,
|
|
390
|
+
resources: {},
|
|
391
|
+
totalWords: 0
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
for (const [key, data] of this.indexes.entries()) {
|
|
395
|
+
const [resourceName, fieldName] = key.split(':');
|
|
396
|
+
|
|
397
|
+
if (!stats.resources[resourceName]) {
|
|
398
|
+
stats.resources[resourceName] = {
|
|
399
|
+
fields: {},
|
|
400
|
+
totalRecords: new Set(),
|
|
401
|
+
totalWords: 0
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!stats.resources[resourceName].fields[fieldName]) {
|
|
406
|
+
stats.resources[resourceName].fields[fieldName] = {
|
|
407
|
+
words: 0,
|
|
408
|
+
totalOccurrences: 0
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
stats.resources[resourceName].fields[fieldName].words++;
|
|
413
|
+
stats.resources[resourceName].fields[fieldName].totalOccurrences += data.count;
|
|
414
|
+
stats.resources[resourceName].totalWords++;
|
|
415
|
+
|
|
416
|
+
for (const recordId of data.recordIds) {
|
|
417
|
+
stats.resources[resourceName].totalRecords.add(recordId);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
stats.totalWords++;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Convert Sets to counts
|
|
424
|
+
for (const resourceName in stats.resources) {
|
|
425
|
+
stats.resources[resourceName].totalRecords = stats.resources[resourceName].totalRecords.size;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return stats;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async rebuildAllIndexes({ timeout } = {}) {
|
|
432
|
+
if (timeout) {
|
|
433
|
+
return Promise.race([
|
|
434
|
+
this._rebuildAllIndexesInternal(),
|
|
435
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout))
|
|
436
|
+
]);
|
|
437
|
+
}
|
|
438
|
+
return this._rebuildAllIndexesInternal();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async _rebuildAllIndexesInternal() {
|
|
442
|
+
const resourceNames = Object.keys(this.database.resources).filter(name => name !== 'fulltext_indexes');
|
|
443
|
+
|
|
444
|
+
// Process resources sequentially to avoid overwhelming the system
|
|
445
|
+
for (const resourceName of resourceNames) {
|
|
446
|
+
const [ok, err] = await tryFn(() => this.rebuildIndex(resourceName));
|
|
447
|
+
if (!ok) {
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async clearIndex(resourceName) {
|
|
453
|
+
// Clear indexes for specific resource
|
|
454
|
+
for (const [key] of this.indexes.entries()) {
|
|
455
|
+
if (key.startsWith(`${resourceName}:`)) {
|
|
456
|
+
this.indexes.delete(key);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Save changes
|
|
461
|
+
await this.saveIndexes();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async clearAllIndexes() {
|
|
465
|
+
// Clear all indexes
|
|
466
|
+
this.indexes.clear();
|
|
467
|
+
|
|
468
|
+
// Save changes
|
|
469
|
+
await this.saveIndexes();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export default FullTextPlugin;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './plugin.class.js'
|
|
2
|
+
export * from './plugin.obj.js'
|
|
3
|
+
export { default as Plugin } from './plugin.class.js'
|
|
4
|
+
|
|
5
|
+
// plugins:
|
|
6
|
+
export * from './audit.plugin.js'
|
|
7
|
+
export * from './cache.plugin.js'
|
|
8
|
+
export * from './costs.plugin.js'
|
|
9
|
+
export * from './fulltext.plugin.js'
|
|
10
|
+
export * from './metrics.plugin.js'
|
|
11
|
+
export * from './queue-consumer.plugin.js'
|
|
12
|
+
export * from './replicator.plugin.js'
|