s3db.js 6.2.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 +372 -469
- package/UNLICENSE +24 -0
- package/dist/s3db.cjs.js +30057 -18387
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +373 -72
- package/dist/s3db.es.js +30043 -18384
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +29730 -18061
- 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,442 @@
|
|
|
1
|
+
import Plugin from "./plugin.class.js";
|
|
2
|
+
import tryFn, { tryFnSync } from "../concerns/try-fn.js";
|
|
3
|
+
|
|
4
|
+
export class AuditPlugin extends Plugin {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
super(options);
|
|
7
|
+
this.auditResource = null;
|
|
8
|
+
this.config = {
|
|
9
|
+
includeData: options.includeData !== false,
|
|
10
|
+
includePartitions: options.includePartitions !== false,
|
|
11
|
+
maxDataSize: options.maxDataSize || 10000, // 10KB limit
|
|
12
|
+
...options
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async onSetup() {
|
|
17
|
+
|
|
18
|
+
// Create audit resource if it doesn't exist
|
|
19
|
+
const [ok, err, auditResource] = await tryFn(() => this.database.createResource({
|
|
20
|
+
name: 'audits',
|
|
21
|
+
attributes: {
|
|
22
|
+
id: 'string|required',
|
|
23
|
+
resourceName: 'string|required',
|
|
24
|
+
operation: 'string|required',
|
|
25
|
+
recordId: 'string|required',
|
|
26
|
+
userId: 'string|optional',
|
|
27
|
+
timestamp: 'string|required',
|
|
28
|
+
oldData: 'string|optional',
|
|
29
|
+
newData: 'string|optional',
|
|
30
|
+
partition: 'string|optional',
|
|
31
|
+
partitionValues: 'string|optional',
|
|
32
|
+
metadata: 'string|optional'
|
|
33
|
+
},
|
|
34
|
+
behavior: 'body-overflow'
|
|
35
|
+
// keyPrefix removido
|
|
36
|
+
}));
|
|
37
|
+
this.auditResource = ok ? auditResource : (this.database.resources.audits || null);
|
|
38
|
+
if (!ok && !this.auditResource) return;
|
|
39
|
+
|
|
40
|
+
this.installDatabaseProxy();
|
|
41
|
+
this.installEventListeners();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async onStart() {
|
|
45
|
+
// Plugin is ready
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async onStop() {
|
|
49
|
+
// Cleanup if needed
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
installDatabaseProxy() {
|
|
53
|
+
if (this.database._auditProxyInstalled) {
|
|
54
|
+
return; // Already installed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const installEventListenersForResource = this.installEventListenersForResource.bind(this);
|
|
58
|
+
|
|
59
|
+
// Store original method
|
|
60
|
+
this.database._originalCreateResource = this.database.createResource;
|
|
61
|
+
|
|
62
|
+
// Create new method that doesn't call itself
|
|
63
|
+
this.database.createResource = async function (...args) {
|
|
64
|
+
const resource = await this._originalCreateResource(...args);
|
|
65
|
+
if (resource.name !== 'audits') {
|
|
66
|
+
installEventListenersForResource(resource);
|
|
67
|
+
}
|
|
68
|
+
return resource;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Mark as installed
|
|
72
|
+
this.database._auditProxyInstalled = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
installEventListeners() {
|
|
76
|
+
for (const resource of Object.values(this.database.resources)) {
|
|
77
|
+
if (resource.name === 'audits') {
|
|
78
|
+
continue; // Don't audit the audit resource
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.installEventListenersForResource(resource);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
installEventListenersForResource(resource) {
|
|
86
|
+
// Store original data for update operations
|
|
87
|
+
const originalDataMap = new Map();
|
|
88
|
+
|
|
89
|
+
// Insert event
|
|
90
|
+
resource.on('insert', async (data) => {
|
|
91
|
+
const recordId = data.id || 'auto-generated';
|
|
92
|
+
|
|
93
|
+
const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
|
|
94
|
+
|
|
95
|
+
const auditRecord = {
|
|
96
|
+
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
97
|
+
resourceName: resource.name,
|
|
98
|
+
operation: 'insert',
|
|
99
|
+
recordId,
|
|
100
|
+
userId: this.getCurrentUserId?.() || 'system',
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
oldData: null,
|
|
103
|
+
newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(data)),
|
|
104
|
+
partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
|
|
105
|
+
partitionValues: this.config.includePartitions ? (partitionValues ? (Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null) : null) : null,
|
|
106
|
+
metadata: JSON.stringify({
|
|
107
|
+
source: 'audit-plugin',
|
|
108
|
+
version: '2.0'
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Log audit asynchronously to avoid blocking
|
|
113
|
+
this.logAudit(auditRecord).catch(console.error);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Update event
|
|
117
|
+
resource.on('update', async (data) => {
|
|
118
|
+
const recordId = data.id;
|
|
119
|
+
let oldData = data.$before;
|
|
120
|
+
|
|
121
|
+
if (this.config.includeData && !oldData) {
|
|
122
|
+
const [ok, err, fetched] = await tryFn(() => resource.get(recordId));
|
|
123
|
+
if (ok) oldData = fetched;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
|
|
127
|
+
|
|
128
|
+
const auditRecord = {
|
|
129
|
+
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
130
|
+
resourceName: resource.name,
|
|
131
|
+
operation: 'update',
|
|
132
|
+
recordId,
|
|
133
|
+
userId: this.getCurrentUserId?.() || 'system',
|
|
134
|
+
timestamp: new Date().toISOString(),
|
|
135
|
+
oldData: oldData && this.config.includeData === false ? null : (oldData ? JSON.stringify(this.truncateData(oldData)) : null),
|
|
136
|
+
newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(data)),
|
|
137
|
+
partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
|
|
138
|
+
partitionValues: this.config.includePartitions ? (partitionValues ? (Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null) : null) : null,
|
|
139
|
+
metadata: JSON.stringify({
|
|
140
|
+
source: 'audit-plugin',
|
|
141
|
+
version: '2.0'
|
|
142
|
+
})
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Log audit asynchronously
|
|
146
|
+
this.logAudit(auditRecord).catch(console.error);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Delete event
|
|
150
|
+
resource.on('delete', async (data) => {
|
|
151
|
+
const recordId = data.id;
|
|
152
|
+
let oldData = data;
|
|
153
|
+
|
|
154
|
+
if (this.config.includeData && !oldData) {
|
|
155
|
+
const [ok, err, fetched] = await tryFn(() => resource.get(recordId));
|
|
156
|
+
if (ok) oldData = fetched;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const partitionValues = oldData && this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null;
|
|
160
|
+
|
|
161
|
+
const auditRecord = {
|
|
162
|
+
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
163
|
+
resourceName: resource.name,
|
|
164
|
+
operation: 'delete',
|
|
165
|
+
recordId,
|
|
166
|
+
userId: this.getCurrentUserId?.() || 'system',
|
|
167
|
+
timestamp: new Date().toISOString(),
|
|
168
|
+
oldData: oldData && this.config.includeData === false ? null : (oldData ? JSON.stringify(this.truncateData(oldData)) : null),
|
|
169
|
+
newData: null,
|
|
170
|
+
partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
|
|
171
|
+
partitionValues: this.config.includePartitions ? (partitionValues ? (Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null) : null) : null,
|
|
172
|
+
metadata: JSON.stringify({
|
|
173
|
+
source: 'audit-plugin',
|
|
174
|
+
version: '2.0'
|
|
175
|
+
})
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Log audit asynchronously
|
|
179
|
+
this.logAudit(auditRecord).catch(console.error);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Remover monkey patch de deleteMany
|
|
183
|
+
// Adicionar middleware para deleteMany
|
|
184
|
+
resource.useMiddleware('deleteMany', async (ctx, next) => {
|
|
185
|
+
const ids = ctx.args[0];
|
|
186
|
+
// Captura os dados antes da deleção
|
|
187
|
+
const oldDataMap = {};
|
|
188
|
+
if (this.config.includeData) {
|
|
189
|
+
for (const id of ids) {
|
|
190
|
+
const [ok, err, data] = await tryFn(() => resource.get(id));
|
|
191
|
+
oldDataMap[id] = ok ? data : null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const result = await next();
|
|
195
|
+
// Auditar depois
|
|
196
|
+
if (result && result.length > 0 && this.config.includeData) {
|
|
197
|
+
for (const id of ids) {
|
|
198
|
+
const oldData = oldDataMap[id];
|
|
199
|
+
const partitionValues = oldData ? (this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null) : null;
|
|
200
|
+
const auditRecord = {
|
|
201
|
+
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
202
|
+
resourceName: resource.name,
|
|
203
|
+
operation: 'delete',
|
|
204
|
+
recordId: id,
|
|
205
|
+
userId: this.getCurrentUserId?.() || 'system',
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
oldData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(oldData)),
|
|
208
|
+
newData: null,
|
|
209
|
+
partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
|
|
210
|
+
partitionValues: this.config.includePartitions ? (partitionValues ? (Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null) : null) : null,
|
|
211
|
+
metadata: JSON.stringify({
|
|
212
|
+
source: 'audit-plugin',
|
|
213
|
+
version: '2.0',
|
|
214
|
+
batchOperation: true
|
|
215
|
+
})
|
|
216
|
+
};
|
|
217
|
+
this.logAudit(auditRecord).catch(console.error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getPartitionValues(data, resource) {
|
|
225
|
+
if (!data) return null;
|
|
226
|
+
const partitions = resource.config?.partitions || {};
|
|
227
|
+
const partitionValues = {};
|
|
228
|
+
|
|
229
|
+
for (const [partitionName, partitionDef] of Object.entries(partitions)) {
|
|
230
|
+
if (partitionDef.fields) {
|
|
231
|
+
const partitionData = {};
|
|
232
|
+
for (const [fieldName, fieldRule] of Object.entries(partitionDef.fields)) {
|
|
233
|
+
// Handle nested fields using dot notation
|
|
234
|
+
const fieldValue = this.getNestedFieldValue(data, fieldName);
|
|
235
|
+
if (fieldValue !== undefined && fieldValue !== null) {
|
|
236
|
+
partitionData[fieldName] = fieldValue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (Object.keys(partitionData).length > 0) {
|
|
240
|
+
partitionValues[partitionName] = partitionData;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return partitionValues;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
getNestedFieldValue(data, fieldPath) {
|
|
249
|
+
// Handle simple field names (no dots)
|
|
250
|
+
if (!fieldPath.includes('.')) {
|
|
251
|
+
return data[fieldPath];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle nested field names using dot notation
|
|
255
|
+
const keys = fieldPath.split('.');
|
|
256
|
+
let currentLevel = data;
|
|
257
|
+
|
|
258
|
+
for (const key of keys) {
|
|
259
|
+
if (!currentLevel || typeof currentLevel !== 'object' || !(key in currentLevel)) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
currentLevel = currentLevel[key];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return currentLevel;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
getPrimaryPartition(partitionValues) {
|
|
269
|
+
if (!partitionValues) return null;
|
|
270
|
+
const partitionNames = Object.keys(partitionValues);
|
|
271
|
+
return partitionNames.length > 0 ? partitionNames[0] : null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async logAudit(auditRecord) {
|
|
275
|
+
if (!auditRecord.id) {
|
|
276
|
+
auditRecord.id = `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
277
|
+
}
|
|
278
|
+
const result = await this.auditResource.insert(auditRecord);
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
truncateData(data) {
|
|
283
|
+
if (!data) return data;
|
|
284
|
+
|
|
285
|
+
// Filter out internal S3DB fields (those starting with _)
|
|
286
|
+
const filteredData = {};
|
|
287
|
+
for (const [key, value] of Object.entries(data)) {
|
|
288
|
+
if (!key.startsWith('_') && key !== '$overflow') {
|
|
289
|
+
filteredData[key] = value;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const dataStr = JSON.stringify(filteredData);
|
|
294
|
+
if (dataStr.length <= this.config.maxDataSize) {
|
|
295
|
+
return filteredData;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Need to actually truncate the data to fit within maxDataSize
|
|
299
|
+
let truncatedData = { ...filteredData };
|
|
300
|
+
let currentSize = JSON.stringify(truncatedData).length;
|
|
301
|
+
|
|
302
|
+
// Reserve space for truncation metadata
|
|
303
|
+
const metadataOverhead = JSON.stringify({
|
|
304
|
+
_truncated: true,
|
|
305
|
+
_originalSize: dataStr.length,
|
|
306
|
+
_truncatedAt: new Date().toISOString()
|
|
307
|
+
}).length;
|
|
308
|
+
|
|
309
|
+
const targetSize = this.config.maxDataSize - metadataOverhead;
|
|
310
|
+
|
|
311
|
+
// Truncate string fields until we fit within the limit
|
|
312
|
+
for (const [key, value] of Object.entries(truncatedData)) {
|
|
313
|
+
if (typeof value === 'string' && currentSize > targetSize) {
|
|
314
|
+
const excess = currentSize - targetSize;
|
|
315
|
+
const newLength = Math.max(0, value.length - excess - 3); // -3 for "..."
|
|
316
|
+
if (newLength < value.length) {
|
|
317
|
+
truncatedData[key] = value.substring(0, newLength) + '...';
|
|
318
|
+
currentSize = JSON.stringify(truncatedData).length;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
...truncatedData,
|
|
325
|
+
_truncated: true,
|
|
326
|
+
_originalSize: dataStr.length,
|
|
327
|
+
_truncatedAt: new Date().toISOString()
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Utility methods for querying audit logs
|
|
332
|
+
async getAuditLogs(options = {}) {
|
|
333
|
+
if (!this.auditResource) return [];
|
|
334
|
+
const [ok, err, result] = await tryFn(async () => {
|
|
335
|
+
const {
|
|
336
|
+
resourceName,
|
|
337
|
+
operation,
|
|
338
|
+
recordId,
|
|
339
|
+
userId,
|
|
340
|
+
partition,
|
|
341
|
+
startDate,
|
|
342
|
+
endDate,
|
|
343
|
+
limit = 100,
|
|
344
|
+
offset = 0
|
|
345
|
+
} = options;
|
|
346
|
+
|
|
347
|
+
const allAudits = await this.auditResource.getAll();
|
|
348
|
+
let filtered = allAudits.filter(audit => {
|
|
349
|
+
if (resourceName && audit.resourceName !== resourceName) return false;
|
|
350
|
+
if (operation && audit.operation !== operation) return false;
|
|
351
|
+
if (recordId && audit.recordId !== recordId) return false;
|
|
352
|
+
if (userId && audit.userId !== userId) return false;
|
|
353
|
+
if (partition && audit.partition !== partition) return false;
|
|
354
|
+
if (startDate && new Date(audit.timestamp) < new Date(startDate)) return false;
|
|
355
|
+
if (endDate && new Date(audit.timestamp) > new Date(endDate)) return false;
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
359
|
+
const deserialized = filtered.slice(offset, offset + limit).map(audit => {
|
|
360
|
+
const [okOld, , oldData] = typeof audit.oldData === 'string' ? tryFnSync(() => JSON.parse(audit.oldData)) : [true, null, audit.oldData];
|
|
361
|
+
const [okNew, , newData] = typeof audit.newData === 'string' ? tryFnSync(() => JSON.parse(audit.newData)) : [true, null, audit.newData];
|
|
362
|
+
const [okPart, , partitionValues] = audit.partitionValues && typeof audit.partitionValues === 'string' ? tryFnSync(() => JSON.parse(audit.partitionValues)) : [true, null, audit.partitionValues];
|
|
363
|
+
const [okMeta, , metadata] = audit.metadata && typeof audit.metadata === 'string' ? tryFnSync(() => JSON.parse(audit.metadata)) : [true, null, audit.metadata];
|
|
364
|
+
return {
|
|
365
|
+
...audit,
|
|
366
|
+
oldData: audit.oldData === null || audit.oldData === undefined || audit.oldData === 'null' ? null : (okOld ? oldData : null),
|
|
367
|
+
newData: audit.newData === null || audit.newData === undefined || audit.newData === 'null' ? null : (okNew ? newData : null),
|
|
368
|
+
partitionValues: okPart ? partitionValues : audit.partitionValues,
|
|
369
|
+
metadata: okMeta ? metadata : audit.metadata
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
return deserialized;
|
|
373
|
+
});
|
|
374
|
+
return ok ? result : [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async getRecordHistory(resourceName, recordId) {
|
|
378
|
+
return this.getAuditLogs({
|
|
379
|
+
resourceName,
|
|
380
|
+
recordId,
|
|
381
|
+
limit: 1000
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async getPartitionHistory(resourceName, partitionName, partitionValues) {
|
|
386
|
+
return this.getAuditLogs({
|
|
387
|
+
resourceName,
|
|
388
|
+
partition: partitionName,
|
|
389
|
+
limit: 1000
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async getAuditStats(options = {}) {
|
|
394
|
+
const {
|
|
395
|
+
resourceName,
|
|
396
|
+
startDate,
|
|
397
|
+
endDate
|
|
398
|
+
} = options;
|
|
399
|
+
|
|
400
|
+
const allAudits = await this.getAuditLogs({
|
|
401
|
+
resourceName,
|
|
402
|
+
startDate,
|
|
403
|
+
endDate,
|
|
404
|
+
limit: 10000
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const stats = {
|
|
408
|
+
total: allAudits.length,
|
|
409
|
+
byOperation: {},
|
|
410
|
+
byResource: {},
|
|
411
|
+
byPartition: {},
|
|
412
|
+
byUser: {},
|
|
413
|
+
timeline: {}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
for (const audit of allAudits) {
|
|
417
|
+
// Count by operation
|
|
418
|
+
stats.byOperation[audit.operation] = (stats.byOperation[audit.operation] || 0) + 1;
|
|
419
|
+
|
|
420
|
+
// Count by resource
|
|
421
|
+
stats.byResource[audit.resourceName] = (stats.byResource[audit.resourceName] || 0) + 1;
|
|
422
|
+
|
|
423
|
+
// Count by partition
|
|
424
|
+
if (audit.partition) {
|
|
425
|
+
stats.byPartition[audit.partition] = (stats.byPartition[audit.partition] || 0) + 1;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Count by user
|
|
429
|
+
stats.byUser[audit.userId] = (stats.byUser[audit.userId] || 0) + 1;
|
|
430
|
+
|
|
431
|
+
// Count by day
|
|
432
|
+
if (audit.timestamp) {
|
|
433
|
+
const day = audit.timestamp.split('T')[0];
|
|
434
|
+
stats.timeline[day] = (stats.timeline[day] || 0) + 1;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return stats;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export default AuditPlugin;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import EventEmitter from "events";
|
|
2
|
+
|
|
3
|
+
export class Cache extends EventEmitter {
|
|
4
|
+
constructor(config = {}) {
|
|
5
|
+
super();
|
|
6
|
+
this.config = config;
|
|
7
|
+
}
|
|
8
|
+
// to implement:
|
|
9
|
+
async _set (key, data) {}
|
|
10
|
+
async _get (key) {}
|
|
11
|
+
async _del (key) {}
|
|
12
|
+
async _clear (key) {}
|
|
13
|
+
|
|
14
|
+
validateKey(key) {
|
|
15
|
+
if (key === null || key === undefined || typeof key !== 'string' || !key) {
|
|
16
|
+
throw new Error('Invalid key');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// generic class methods
|
|
21
|
+
async set(key, data) {
|
|
22
|
+
this.validateKey(key);
|
|
23
|
+
await this._set(key, data);
|
|
24
|
+
this.emit("set", data);
|
|
25
|
+
return data
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async get(key) {
|
|
29
|
+
this.validateKey(key);
|
|
30
|
+
const data = await this._get(key);
|
|
31
|
+
this.emit("get", data);
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async del(key) {
|
|
36
|
+
this.validateKey(key);
|
|
37
|
+
const data = await this._del(key);
|
|
38
|
+
this.emit("delete", data);
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async delete(key) {
|
|
43
|
+
return this.del(key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async clear(prefix) {
|
|
47
|
+
const data = await this._clear(prefix);
|
|
48
|
+
this.emit("clear", data);
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default Cache
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Cache Configuration Documentation
|
|
3
|
+
*
|
|
4
|
+
* This cache implementation stores data in memory using a Map-like structure.
|
|
5
|
+
* It provides fast access to frequently used data but is limited by available RAM
|
|
6
|
+
* and data is lost when the process restarts.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} MemoryCacheConfig
|
|
9
|
+
* @property {number} [maxSize=1000] - Maximum number of items to store in cache
|
|
10
|
+
* @property {number} [ttl=300000] - Time to live in milliseconds (5 minutes default)
|
|
11
|
+
* @property {boolean} [enableStats=false] - Whether to track cache statistics (hits, misses, etc.)
|
|
12
|
+
* @property {string} [evictionPolicy='lru'] - Cache eviction policy: 'lru' (Least Recently Used) or 'fifo' (First In First Out)
|
|
13
|
+
* @property {boolean} [logEvictions=false] - Whether to log when items are evicted from cache
|
|
14
|
+
* @property {number} [cleanupInterval=60000] - Interval in milliseconds to run cleanup of expired items (1 minute default)
|
|
15
|
+
* @property {boolean} [caseSensitive=true] - Whether cache keys are case sensitive
|
|
16
|
+
* @property {Function} [serializer] - Custom function to serialize values before storage
|
|
17
|
+
* - Parameters: (value: any) => string
|
|
18
|
+
* - Default: JSON.stringify
|
|
19
|
+
* @property {Function} [deserializer] - Custom function to deserialize values after retrieval
|
|
20
|
+
* - Parameters: (string: string) => any
|
|
21
|
+
* - Default: JSON.parse
|
|
22
|
+
* @property {boolean} [enableCompression=false] - Whether to compress values using gzip (requires zlib)
|
|
23
|
+
* @property {number} [compressionThreshold=1024] - Minimum size in bytes to trigger compression
|
|
24
|
+
* @property {Object} [tags] - Default tags to apply to all cached items
|
|
25
|
+
* - Key: tag name (e.g., 'environment', 'version')
|
|
26
|
+
* - Value: tag value (e.g., 'production', '1.0.0')
|
|
27
|
+
* @property {boolean} [persistent=false] - Whether to persist cache to disk (experimental)
|
|
28
|
+
* @property {string} [persistencePath='./cache'] - Directory path for persistent cache storage
|
|
29
|
+
* @property {number} [persistenceInterval=300000] - Interval in milliseconds to save cache to disk (5 minutes default)
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Basic configuration with LRU eviction
|
|
33
|
+
* {
|
|
34
|
+
* maxSize: 5000,
|
|
35
|
+
* ttl: 600000, // 10 minutes
|
|
36
|
+
* evictionPolicy: 'lru',
|
|
37
|
+
* enableStats: true,
|
|
38
|
+
* logEvictions: true
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Configuration with compression and custom serialization
|
|
43
|
+
* {
|
|
44
|
+
* maxSize: 10000,
|
|
45
|
+
* ttl: 1800000, // 30 minutes
|
|
46
|
+
* enableCompression: true,
|
|
47
|
+
* compressionThreshold: 512,
|
|
48
|
+
* serializer: (value) => Buffer.from(JSON.stringify(value)).toString('base64'),
|
|
49
|
+
* deserializer: (str) => JSON.parse(Buffer.from(str, 'base64').toString()),
|
|
50
|
+
* tags: {
|
|
51
|
+
* 'environment': 'production',
|
|
52
|
+
* 'cache_type': 'memory'
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // FIFO configuration with persistent storage
|
|
58
|
+
* {
|
|
59
|
+
* maxSize: 2000,
|
|
60
|
+
* ttl: 900000, // 15 minutes
|
|
61
|
+
* evictionPolicy: 'fifo',
|
|
62
|
+
* persistent: true,
|
|
63
|
+
* persistencePath: './data/cache',
|
|
64
|
+
* persistenceInterval: 600000 // 10 minutes
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Minimal configuration using defaults
|
|
69
|
+
* {
|
|
70
|
+
* maxSize: 1000,
|
|
71
|
+
* ttl: 300000 // 5 minutes
|
|
72
|
+
* }
|
|
73
|
+
*
|
|
74
|
+
* @notes
|
|
75
|
+
* - Memory usage is limited by available RAM and maxSize setting
|
|
76
|
+
* - TTL is checked on access, not automatically in background
|
|
77
|
+
* - LRU eviction removes least recently accessed items when cache is full
|
|
78
|
+
* - FIFO eviction removes oldest items when cache is full
|
|
79
|
+
* - Statistics include hit rate, miss rate, and eviction count
|
|
80
|
+
* - Compression reduces memory usage but increases CPU overhead
|
|
81
|
+
* - Custom serializers allow for specialized data formats
|
|
82
|
+
* - Persistent storage survives process restarts but may be slower
|
|
83
|
+
* - Cleanup interval helps prevent memory leaks from expired items
|
|
84
|
+
* - Tags are useful for cache invalidation and monitoring
|
|
85
|
+
* - Case sensitivity affects key matching and storage efficiency
|
|
86
|
+
*/
|
|
87
|
+
import { Cache } from "./cache.class.js"
|
|
88
|
+
|
|
89
|
+
export class MemoryCache extends Cache {
|
|
90
|
+
constructor(config = {}) {
|
|
91
|
+
super(config);
|
|
92
|
+
this.cache = {};
|
|
93
|
+
this.meta = {};
|
|
94
|
+
this.maxSize = config.maxSize || 0;
|
|
95
|
+
this.ttl = config.ttl || 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async _set(key, data) {
|
|
99
|
+
// Limpar se exceder maxSize
|
|
100
|
+
if (this.maxSize > 0 && Object.keys(this.cache).length >= this.maxSize) {
|
|
101
|
+
// Remove o item mais antigo
|
|
102
|
+
const oldestKey = Object.entries(this.meta)
|
|
103
|
+
.sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
|
|
104
|
+
if (oldestKey) {
|
|
105
|
+
delete this.cache[oldestKey];
|
|
106
|
+
delete this.meta[oldestKey];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.cache[key] = data;
|
|
110
|
+
this.meta[key] = { ts: Date.now() };
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async _get(key) {
|
|
115
|
+
if (!Object.prototype.hasOwnProperty.call(this.cache, key)) return null;
|
|
116
|
+
if (this.ttl > 0) {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const meta = this.meta[key];
|
|
119
|
+
if (meta && now - meta.ts > this.ttl * 1000) {
|
|
120
|
+
// Expirado
|
|
121
|
+
delete this.cache[key];
|
|
122
|
+
delete this.meta[key];
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return this.cache[key];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async _del(key) {
|
|
130
|
+
delete this.cache[key];
|
|
131
|
+
delete this.meta[key];
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async _clear(prefix) {
|
|
136
|
+
if (!prefix) {
|
|
137
|
+
this.cache = {};
|
|
138
|
+
this.meta = {};
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
// Remove only keys that start with the prefix
|
|
142
|
+
const removed = [];
|
|
143
|
+
for (const key of Object.keys(this.cache)) {
|
|
144
|
+
if (key.startsWith(prefix)) {
|
|
145
|
+
removed.push(key);
|
|
146
|
+
delete this.cache[key];
|
|
147
|
+
delete this.meta[key];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (removed.length > 0) {
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async size() {
|
|
156
|
+
return Object.keys(this.cache).length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async keys() {
|
|
160
|
+
return Object.keys(this.cache);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default MemoryCache
|