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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Plugin from "./plugin.class.js";
|
|
2
|
-
import tryFn
|
|
2
|
+
import tryFn from "../concerns/try-fn.js";
|
|
3
3
|
|
|
4
4
|
export class AuditPlugin extends Plugin {
|
|
5
5
|
constructor(options = {}) {
|
|
@@ -8,14 +8,13 @@ export class AuditPlugin extends Plugin {
|
|
|
8
8
|
this.config = {
|
|
9
9
|
includeData: options.includeData !== false,
|
|
10
10
|
includePartitions: options.includePartitions !== false,
|
|
11
|
-
maxDataSize: options.maxDataSize || 10000,
|
|
11
|
+
maxDataSize: options.maxDataSize || 10000,
|
|
12
12
|
...options
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
async onSetup() {
|
|
17
|
-
|
|
18
|
-
// Create audit resource if it doesn't exist
|
|
17
|
+
// Create audit resource
|
|
19
18
|
const [ok, err, auditResource] = await tryFn(() => this.database.createResource({
|
|
20
19
|
name: 'audits',
|
|
21
20
|
attributes: {
|
|
@@ -32,237 +31,155 @@ export class AuditPlugin extends Plugin {
|
|
|
32
31
|
metadata: 'string|optional'
|
|
33
32
|
},
|
|
34
33
|
behavior: 'body-overflow'
|
|
35
|
-
// keyPrefix removido
|
|
36
34
|
}));
|
|
37
35
|
this.auditResource = ok ? auditResource : (this.database.resources.audits || null);
|
|
38
36
|
if (!ok && !this.auditResource) return;
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
this.
|
|
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);
|
|
38
|
+
// Hook into database for new resources
|
|
39
|
+
this.database.addHook('afterCreateResource', (context) => {
|
|
40
|
+
if (context.resource.name !== 'audits') {
|
|
41
|
+
this.setupResourceAuditing(context.resource);
|
|
67
42
|
}
|
|
68
|
-
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Mark as installed
|
|
72
|
-
this.database._auditProxyInstalled = true;
|
|
73
|
-
}
|
|
43
|
+
});
|
|
74
44
|
|
|
75
|
-
|
|
45
|
+
// Setup existing resources
|
|
76
46
|
for (const resource of Object.values(this.database.resources)) {
|
|
77
|
-
if (resource.name
|
|
78
|
-
|
|
47
|
+
if (resource.name !== 'audits') {
|
|
48
|
+
this.setupResourceAuditing(resource);
|
|
79
49
|
}
|
|
80
|
-
|
|
81
|
-
this.installEventListenersForResource(resource);
|
|
82
50
|
}
|
|
83
51
|
}
|
|
84
52
|
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
53
|
+
async onStart() {
|
|
54
|
+
// Ready
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async onStop() {
|
|
58
|
+
// No cleanup needed
|
|
59
|
+
}
|
|
88
60
|
|
|
89
|
-
|
|
61
|
+
setupResourceAuditing(resource) {
|
|
62
|
+
// Insert
|
|
90
63
|
resource.on('insert', async (data) => {
|
|
91
|
-
|
|
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)}`,
|
|
64
|
+
await this.logAudit({
|
|
97
65
|
resourceName: resource.name,
|
|
98
66
|
operation: 'insert',
|
|
99
|
-
recordId,
|
|
100
|
-
userId: this.getCurrentUserId?.() || 'system',
|
|
101
|
-
timestamp: new Date().toISOString(),
|
|
67
|
+
recordId: data.id || 'auto-generated',
|
|
102
68
|
oldData: null,
|
|
103
|
-
newData: this.config.includeData
|
|
104
|
-
partition: this.config.includePartitions ? this.getPrimaryPartition(
|
|
105
|
-
partitionValues: this.config.includePartitions
|
|
106
|
-
|
|
107
|
-
source: 'audit-plugin',
|
|
108
|
-
version: '2.0'
|
|
109
|
-
})
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Log audit asynchronously to avoid blocking
|
|
113
|
-
this.logAudit(auditRecord).catch(() => {});
|
|
69
|
+
newData: this.config.includeData ? JSON.stringify(this.truncateData(data)) : null,
|
|
70
|
+
partition: this.config.includePartitions && this.getPartitionValues(data, resource) ? this.getPrimaryPartition(this.getPartitionValues(data, resource)) : null,
|
|
71
|
+
partitionValues: this.config.includePartitions && this.getPartitionValues(data, resource) ? JSON.stringify(this.getPartitionValues(data, resource)) : null
|
|
72
|
+
});
|
|
114
73
|
});
|
|
115
74
|
|
|
116
|
-
// Update
|
|
75
|
+
// Update
|
|
117
76
|
resource.on('update', async (data) => {
|
|
118
|
-
const recordId = data.id;
|
|
119
77
|
let oldData = data.$before;
|
|
120
|
-
|
|
121
78
|
if (this.config.includeData && !oldData) {
|
|
122
|
-
const [ok, err, fetched] = await tryFn(() => resource.get(
|
|
79
|
+
const [ok, err, fetched] = await tryFn(() => resource.get(data.id));
|
|
123
80
|
if (ok) oldData = fetched;
|
|
124
81
|
}
|
|
125
82
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const auditRecord = {
|
|
129
|
-
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
83
|
+
await this.logAudit({
|
|
130
84
|
resourceName: resource.name,
|
|
131
85
|
operation: 'update',
|
|
132
|
-
recordId,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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(() => {});
|
|
86
|
+
recordId: data.id,
|
|
87
|
+
oldData: oldData && this.config.includeData ? JSON.stringify(this.truncateData(oldData)) : null,
|
|
88
|
+
newData: this.config.includeData ? JSON.stringify(this.truncateData(data)) : null,
|
|
89
|
+
partition: this.config.includePartitions && this.getPartitionValues(data, resource) ? this.getPrimaryPartition(this.getPartitionValues(data, resource)) : null,
|
|
90
|
+
partitionValues: this.config.includePartitions && this.getPartitionValues(data, resource) ? JSON.stringify(this.getPartitionValues(data, resource)) : null
|
|
91
|
+
});
|
|
147
92
|
});
|
|
148
93
|
|
|
149
|
-
// Delete
|
|
94
|
+
// Delete
|
|
150
95
|
resource.on('delete', async (data) => {
|
|
151
|
-
const recordId = data.id;
|
|
152
96
|
let oldData = data;
|
|
153
|
-
|
|
154
97
|
if (this.config.includeData && !oldData) {
|
|
155
|
-
const [ok, err, fetched] = await tryFn(() => resource.get(
|
|
98
|
+
const [ok, err, fetched] = await tryFn(() => resource.get(data.id));
|
|
156
99
|
if (ok) oldData = fetched;
|
|
157
100
|
}
|
|
158
101
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const auditRecord = {
|
|
162
|
-
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
102
|
+
await this.logAudit({
|
|
163
103
|
resourceName: resource.name,
|
|
164
104
|
operation: 'delete',
|
|
165
|
-
recordId,
|
|
166
|
-
|
|
167
|
-
timestamp: new Date().toISOString(),
|
|
168
|
-
oldData: oldData && this.config.includeData === false ? null : (oldData ? JSON.stringify(this.truncateData(oldData)) : null),
|
|
105
|
+
recordId: data.id,
|
|
106
|
+
oldData: oldData && this.config.includeData ? JSON.stringify(this.truncateData(oldData)) : null,
|
|
169
107
|
newData: null,
|
|
170
|
-
partition: this.config.includePartitions ? this.getPrimaryPartition(
|
|
171
|
-
partitionValues: this.config.includePartitions
|
|
172
|
-
|
|
173
|
-
source: 'audit-plugin',
|
|
174
|
-
version: '2.0'
|
|
175
|
-
})
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// Log audit asynchronously
|
|
179
|
-
this.logAudit(auditRecord).catch(() => {});
|
|
108
|
+
partition: oldData && this.config.includePartitions && this.getPartitionValues(oldData, resource) ? this.getPrimaryPartition(this.getPartitionValues(oldData, resource)) : null,
|
|
109
|
+
partitionValues: oldData && this.config.includePartitions && this.getPartitionValues(oldData, resource) ? JSON.stringify(this.getPartitionValues(oldData, resource)) : null
|
|
110
|
+
});
|
|
180
111
|
});
|
|
112
|
+
}
|
|
181
113
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
114
|
+
// Backward compatibility for tests
|
|
115
|
+
installEventListenersForResource(resource) {
|
|
116
|
+
return this.setupResourceAuditing(resource);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async logAudit(auditData) {
|
|
120
|
+
if (!this.auditResource) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const auditRecord = {
|
|
125
|
+
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
126
|
+
userId: this.getCurrentUserId?.() || 'system',
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
metadata: JSON.stringify({ source: 'audit-plugin', version: '2.0' }),
|
|
129
|
+
resourceName: auditData.resourceName,
|
|
130
|
+
operation: auditData.operation,
|
|
131
|
+
recordId: auditData.recordId
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Only add fields that are not null
|
|
135
|
+
if (auditData.oldData !== null) {
|
|
136
|
+
auditRecord.oldData = auditData.oldData;
|
|
137
|
+
}
|
|
138
|
+
if (auditData.newData !== null) {
|
|
139
|
+
auditRecord.newData = auditData.newData;
|
|
140
|
+
}
|
|
141
|
+
if (auditData.partition !== null) {
|
|
142
|
+
auditRecord.partition = auditData.partition;
|
|
143
|
+
}
|
|
144
|
+
if (auditData.partitionValues !== null) {
|
|
145
|
+
auditRecord.partitionValues = auditData.partitionValues;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await this.auditResource.insert(auditRecord);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Silently fail to avoid breaking operations
|
|
152
|
+
console.warn('Audit logging failed:', error.message);
|
|
153
|
+
}
|
|
222
154
|
}
|
|
223
155
|
|
|
224
156
|
getPartitionValues(data, resource) {
|
|
225
|
-
if (!
|
|
226
|
-
const partitions = resource.config?.partitions || {};
|
|
227
|
-
const partitionValues = {};
|
|
157
|
+
if (!this.config.includePartitions || !resource.partitions) return null;
|
|
228
158
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (Object.keys(partitionData).length > 0) {
|
|
240
|
-
partitionValues[partitionName] = partitionData;
|
|
241
|
-
}
|
|
159
|
+
const partitionValues = {};
|
|
160
|
+
for (const [partitionName, partitionConfig] of Object.entries(resource.partitions)) {
|
|
161
|
+
const values = {};
|
|
162
|
+
for (const field of Object.keys(partitionConfig.fields)) {
|
|
163
|
+
values[field] = this.getNestedFieldValue(data, field);
|
|
164
|
+
}
|
|
165
|
+
if (Object.values(values).some(v => v !== undefined && v !== null)) {
|
|
166
|
+
partitionValues[partitionName] = values;
|
|
242
167
|
}
|
|
243
168
|
}
|
|
244
|
-
|
|
245
|
-
return partitionValues;
|
|
169
|
+
return Object.keys(partitionValues).length > 0 ? partitionValues : null;
|
|
246
170
|
}
|
|
247
171
|
|
|
248
172
|
getNestedFieldValue(data, fieldPath) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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)) {
|
|
173
|
+
const parts = fieldPath.split('.');
|
|
174
|
+
let value = data;
|
|
175
|
+
for (const part of parts) {
|
|
176
|
+
if (value && typeof value === 'object' && part in value) {
|
|
177
|
+
value = value[part];
|
|
178
|
+
} else {
|
|
260
179
|
return undefined;
|
|
261
180
|
}
|
|
262
|
-
currentLevel = currentLevel[key];
|
|
263
181
|
}
|
|
264
|
-
|
|
265
|
-
return currentLevel;
|
|
182
|
+
return value;
|
|
266
183
|
}
|
|
267
184
|
|
|
268
185
|
getPrimaryPartition(partitionValues) {
|
|
@@ -271,172 +188,88 @@ export class AuditPlugin extends Plugin {
|
|
|
271
188
|
return partitionNames.length > 0 ? partitionNames[0] : null;
|
|
272
189
|
}
|
|
273
190
|
|
|
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
191
|
truncateData(data) {
|
|
283
|
-
if (!
|
|
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
|
-
}
|
|
192
|
+
if (!this.config.includeData) return null;
|
|
292
193
|
|
|
293
|
-
const dataStr = JSON.stringify(
|
|
194
|
+
const dataStr = JSON.stringify(data);
|
|
294
195
|
if (dataStr.length <= this.config.maxDataSize) {
|
|
295
|
-
return
|
|
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
|
-
}
|
|
196
|
+
return data;
|
|
321
197
|
}
|
|
322
|
-
|
|
198
|
+
|
|
323
199
|
return {
|
|
324
|
-
...
|
|
200
|
+
...data,
|
|
325
201
|
_truncated: true,
|
|
326
202
|
_originalSize: dataStr.length,
|
|
327
203
|
_truncatedAt: new Date().toISOString()
|
|
328
204
|
};
|
|
329
205
|
}
|
|
330
206
|
|
|
331
|
-
// Utility methods for querying audit logs
|
|
332
207
|
async getAuditLogs(options = {}) {
|
|
333
208
|
if (!this.auditResource) return [];
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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 : [];
|
|
209
|
+
|
|
210
|
+
const { resourceName, operation, recordId, partition, startDate, endDate, limit = 100 } = options;
|
|
211
|
+
|
|
212
|
+
let query = {};
|
|
213
|
+
|
|
214
|
+
if (resourceName) query.resourceName = resourceName;
|
|
215
|
+
if (operation) query.operation = operation;
|
|
216
|
+
if (recordId) query.recordId = recordId;
|
|
217
|
+
if (partition) query.partition = partition;
|
|
218
|
+
|
|
219
|
+
if (startDate || endDate) {
|
|
220
|
+
query.timestamp = {};
|
|
221
|
+
if (startDate) query.timestamp.$gte = startDate;
|
|
222
|
+
if (endDate) query.timestamp.$lte = endDate;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = await this.auditResource.page({ query, limit });
|
|
226
|
+
return result.items || [];
|
|
375
227
|
}
|
|
376
228
|
|
|
377
229
|
async getRecordHistory(resourceName, recordId) {
|
|
378
|
-
return this.getAuditLogs({
|
|
379
|
-
resourceName,
|
|
380
|
-
recordId,
|
|
381
|
-
limit: 1000
|
|
382
|
-
});
|
|
230
|
+
return await this.getAuditLogs({ resourceName, recordId });
|
|
383
231
|
}
|
|
384
232
|
|
|
385
233
|
async getPartitionHistory(resourceName, partitionName, partitionValues) {
|
|
386
|
-
return this.getAuditLogs({
|
|
387
|
-
resourceName,
|
|
234
|
+
return await this.getAuditLogs({
|
|
235
|
+
resourceName,
|
|
388
236
|
partition: partitionName,
|
|
389
|
-
|
|
237
|
+
partitionValues: JSON.stringify(partitionValues)
|
|
390
238
|
});
|
|
391
239
|
}
|
|
392
240
|
|
|
393
241
|
async getAuditStats(options = {}) {
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
startDate,
|
|
397
|
-
endDate
|
|
398
|
-
} = options;
|
|
399
|
-
|
|
400
|
-
const allAudits = await this.getAuditLogs({
|
|
401
|
-
resourceName,
|
|
402
|
-
startDate,
|
|
403
|
-
endDate,
|
|
404
|
-
limit: 10000
|
|
405
|
-
});
|
|
406
|
-
|
|
242
|
+
const logs = await this.getAuditLogs(options);
|
|
243
|
+
|
|
407
244
|
const stats = {
|
|
408
|
-
total:
|
|
245
|
+
total: logs.length,
|
|
409
246
|
byOperation: {},
|
|
410
247
|
byResource: {},
|
|
411
248
|
byPartition: {},
|
|
412
249
|
byUser: {},
|
|
413
250
|
timeline: {}
|
|
414
251
|
};
|
|
415
|
-
|
|
416
|
-
for (const
|
|
252
|
+
|
|
253
|
+
for (const log of logs) {
|
|
417
254
|
// Count by operation
|
|
418
|
-
stats.byOperation[
|
|
255
|
+
stats.byOperation[log.operation] = (stats.byOperation[log.operation] || 0) + 1;
|
|
419
256
|
|
|
420
257
|
// Count by resource
|
|
421
|
-
stats.byResource[
|
|
258
|
+
stats.byResource[log.resourceName] = (stats.byResource[log.resourceName] || 0) + 1;
|
|
422
259
|
|
|
423
260
|
// Count by partition
|
|
424
|
-
if (
|
|
425
|
-
stats.byPartition[
|
|
261
|
+
if (log.partition) {
|
|
262
|
+
stats.byPartition[log.partition] = (stats.byPartition[log.partition] || 0) + 1;
|
|
426
263
|
}
|
|
427
264
|
|
|
428
265
|
// Count by user
|
|
429
|
-
stats.byUser[
|
|
266
|
+
stats.byUser[log.userId] = (stats.byUser[log.userId] || 0) + 1;
|
|
430
267
|
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
stats.timeline[day] = (stats.timeline[day] || 0) + 1;
|
|
435
|
-
}
|
|
268
|
+
// Timeline by date
|
|
269
|
+
const date = log.timestamp.split('T')[0];
|
|
270
|
+
stats.timeline[date] = (stats.timeline[date] || 0) + 1;
|
|
436
271
|
}
|
|
437
|
-
|
|
272
|
+
|
|
438
273
|
return stats;
|
|
439
274
|
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
export default AuditPlugin;
|
|
275
|
+
}
|
|
@@ -455,24 +455,45 @@ export class FilesystemCache extends Cache {
|
|
|
455
455
|
for (const file of cacheFiles) {
|
|
456
456
|
const filePath = path.join(this.directory, file);
|
|
457
457
|
|
|
458
|
-
// Delete main file
|
|
459
|
-
|
|
460
|
-
await
|
|
458
|
+
// Delete main file (handle ENOENT gracefully)
|
|
459
|
+
try {
|
|
460
|
+
if (await this._fileExists(filePath)) {
|
|
461
|
+
await unlink(filePath);
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (error.code !== 'ENOENT') {
|
|
465
|
+
throw error; // Re-throw non-ENOENT errors
|
|
466
|
+
}
|
|
467
|
+
// ENOENT means file is already gone, which is what we wanted
|
|
461
468
|
}
|
|
462
469
|
|
|
463
|
-
// Delete metadata file
|
|
470
|
+
// Delete metadata file (handle ENOENT gracefully)
|
|
464
471
|
if (this.enableMetadata) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
await
|
|
472
|
+
try {
|
|
473
|
+
const metadataPath = this._getMetadataPath(filePath);
|
|
474
|
+
if (await this._fileExists(metadataPath)) {
|
|
475
|
+
await unlink(metadataPath);
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (error.code !== 'ENOENT') {
|
|
479
|
+
throw error; // Re-throw non-ENOENT errors
|
|
480
|
+
}
|
|
481
|
+
// ENOENT means file is already gone, which is what we wanted
|
|
468
482
|
}
|
|
469
483
|
}
|
|
470
484
|
|
|
471
|
-
// Delete backup file
|
|
485
|
+
// Delete backup file (handle ENOENT gracefully)
|
|
472
486
|
if (this.enableBackup) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
await
|
|
487
|
+
try {
|
|
488
|
+
const backupPath = filePath + this.backupSuffix;
|
|
489
|
+
if (await this._fileExists(backupPath)) {
|
|
490
|
+
await unlink(backupPath);
|
|
491
|
+
}
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (error.code !== 'ENOENT') {
|
|
494
|
+
throw error; // Re-throw non-ENOENT errors
|
|
495
|
+
}
|
|
496
|
+
// ENOENT means file is already gone, which is what we wanted
|
|
476
497
|
}
|
|
477
498
|
}
|
|
478
499
|
}
|
|
@@ -490,6 +511,14 @@ export class FilesystemCache extends Cache {
|
|
|
490
511
|
return true;
|
|
491
512
|
|
|
492
513
|
} catch (error) {
|
|
514
|
+
// Handle ENOENT errors at the top level too (e.g., directory doesn't exist)
|
|
515
|
+
if (error.code === 'ENOENT') {
|
|
516
|
+
if (this.enableStats) {
|
|
517
|
+
this.stats.clears++;
|
|
518
|
+
}
|
|
519
|
+
return true; // Already cleared!
|
|
520
|
+
}
|
|
521
|
+
|
|
493
522
|
if (this.enableStats) {
|
|
494
523
|
this.stats.errors++;
|
|
495
524
|
}
|