@soulcraft/brainy 2.0.2 → 2.3.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/README.md +2 -2
- package/dist/augmentations/AugmentationMetadataContract.d.ts +94 -0
- package/dist/augmentations/AugmentationMetadataContract.js +306 -0
- package/dist/augmentations/apiServerAugmentation.d.ts +1 -0
- package/dist/augmentations/apiServerAugmentation.js +1 -0
- package/dist/augmentations/batchProcessingAugmentation.d.ts +1 -0
- package/dist/augmentations/batchProcessingAugmentation.js +1 -0
- package/dist/augmentations/brainyAugmentation.d.ts +16 -0
- package/dist/augmentations/cacheAugmentation.d.ts +1 -0
- package/dist/augmentations/cacheAugmentation.js +1 -0
- package/dist/augmentations/conduitAugmentations.d.ts +1 -0
- package/dist/augmentations/conduitAugmentations.js +1 -0
- package/dist/augmentations/connectionPoolAugmentation.d.ts +1 -0
- package/dist/augmentations/connectionPoolAugmentation.js +1 -0
- package/dist/augmentations/entityRegistryAugmentation.d.ts +2 -0
- package/dist/augmentations/entityRegistryAugmentation.js +2 -0
- package/dist/augmentations/indexAugmentation.d.ts +1 -0
- package/dist/augmentations/indexAugmentation.js +1 -0
- package/dist/augmentations/intelligentVerbScoringAugmentation.d.ts +4 -0
- package/dist/augmentations/intelligentVerbScoringAugmentation.js +4 -0
- package/dist/augmentations/metadataEnforcer.d.ts +20 -0
- package/dist/augmentations/metadataEnforcer.js +171 -0
- package/dist/augmentations/metricsAugmentation.d.ts +2 -7
- package/dist/augmentations/metricsAugmentation.js +1 -0
- package/dist/augmentations/monitoringAugmentation.d.ts +1 -0
- package/dist/augmentations/monitoringAugmentation.js +1 -0
- package/dist/augmentations/neuralImport.d.ts +16 -3
- package/dist/augmentations/neuralImport.js +199 -55
- package/dist/augmentations/requestDeduplicatorAugmentation.d.ts +1 -0
- package/dist/augmentations/requestDeduplicatorAugmentation.js +1 -0
- package/dist/augmentations/serverSearchAugmentations.d.ts +2 -0
- package/dist/augmentations/serverSearchAugmentations.js +2 -0
- package/dist/augmentations/storageAugmentation.d.ts +1 -0
- package/dist/augmentations/storageAugmentation.js +1 -0
- package/dist/augmentations/synapseAugmentation.d.ts +4 -0
- package/dist/augmentations/synapseAugmentation.js +4 -0
- package/dist/augmentations/typeMatching/intelligentTypeMatcher.d.ts +83 -0
- package/dist/augmentations/typeMatching/intelligentTypeMatcher.js +425 -0
- package/dist/augmentations/walAugmentation.d.ts +1 -0
- package/dist/augmentations/walAugmentation.js +1 -0
- package/dist/brainyData.d.ts +32 -5
- package/dist/brainyData.js +263 -111
- package/dist/importManager.d.ts +78 -0
- package/dist/importManager.js +258 -0
- package/dist/neural/embeddedPatterns.d.ts +1 -1
- package/dist/neural/embeddedPatterns.js +1 -1
- package/dist/triple/TripleIntelligence.d.ts +4 -0
- package/dist/triple/TripleIntelligence.js +39 -9
- package/dist/utils/deletedItemsIndex.d.ts +59 -0
- package/dist/utils/deletedItemsIndex.js +98 -0
- package/dist/utils/ensureDeleted.d.ts +38 -0
- package/dist/utils/ensureDeleted.js +79 -0
- package/dist/utils/metadataFilter.js +5 -0
- package/dist/utils/metadataIndex.d.ts +4 -0
- package/dist/utils/metadataIndex.js +45 -0
- package/dist/utils/metadataNamespace.d.ts +113 -0
- package/dist/utils/metadataNamespace.js +162 -0
- package/dist/utils/periodicCleanup.d.ts +87 -0
- package/dist/utils/periodicCleanup.js +219 -0
- package/package.json +13 -5
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import Manager - Comprehensive data import with intelligent type detection
|
|
3
|
+
*
|
|
4
|
+
* Handles multiple data sources:
|
|
5
|
+
* - Direct data (objects, arrays)
|
|
6
|
+
* - Files (JSON, CSV, text)
|
|
7
|
+
* - URLs (fetch and parse)
|
|
8
|
+
* - Streams (for large files)
|
|
9
|
+
*
|
|
10
|
+
* Uses NeuralImportAugmentation for intelligent processing
|
|
11
|
+
*/
|
|
12
|
+
import { VerbType } from './types/graphTypes.js';
|
|
13
|
+
import { NeuralImportAugmentation } from './augmentations/neuralImport.js';
|
|
14
|
+
import * as fs from './universal/fs.js';
|
|
15
|
+
import * as path from './universal/path.js';
|
|
16
|
+
import { prodLog } from './utils/logger.js';
|
|
17
|
+
export class ImportManager {
|
|
18
|
+
constructor(brain) {
|
|
19
|
+
this.typeMatcher = null;
|
|
20
|
+
this.brain = brain;
|
|
21
|
+
this.neuralImport = new NeuralImportAugmentation();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the import manager
|
|
25
|
+
*/
|
|
26
|
+
async init() {
|
|
27
|
+
// Initialize neural import with proper context
|
|
28
|
+
const context = {
|
|
29
|
+
brain: this.brain,
|
|
30
|
+
storage: this.brain.storage,
|
|
31
|
+
config: {},
|
|
32
|
+
log: (message, level) => {
|
|
33
|
+
if (level === 'error') {
|
|
34
|
+
prodLog.error(message);
|
|
35
|
+
}
|
|
36
|
+
else if (level === 'warn') {
|
|
37
|
+
prodLog.warn(message);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
prodLog.info(message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
await this.neuralImport.initialize(context);
|
|
45
|
+
// Get type matcher
|
|
46
|
+
const { getTypeMatcher } = await import('./augmentations/typeMatching/intelligentTypeMatcher.js');
|
|
47
|
+
this.typeMatcher = await getTypeMatcher();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Main import method - handles all sources
|
|
51
|
+
*/
|
|
52
|
+
async import(source, options = {}) {
|
|
53
|
+
const result = {
|
|
54
|
+
success: false,
|
|
55
|
+
nouns: [],
|
|
56
|
+
verbs: [],
|
|
57
|
+
errors: [],
|
|
58
|
+
stats: {
|
|
59
|
+
total: 0,
|
|
60
|
+
imported: 0,
|
|
61
|
+
failed: 0,
|
|
62
|
+
relationships: 0
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
// Detect source type
|
|
67
|
+
const sourceType = await this.detectSourceType(source, options.source);
|
|
68
|
+
// Get data based on source type
|
|
69
|
+
let data;
|
|
70
|
+
let format = options.format || 'auto';
|
|
71
|
+
switch (sourceType) {
|
|
72
|
+
case 'url':
|
|
73
|
+
data = await this.fetchFromUrl(source);
|
|
74
|
+
break;
|
|
75
|
+
case 'file':
|
|
76
|
+
const filePath = source;
|
|
77
|
+
data = await this.readFile(filePath);
|
|
78
|
+
if (format === 'auto') {
|
|
79
|
+
format = this.detectFormatFromPath(filePath);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case 'data':
|
|
83
|
+
default:
|
|
84
|
+
data = source;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
// Process data through neural import
|
|
88
|
+
let items;
|
|
89
|
+
let relationships = [];
|
|
90
|
+
if (Buffer.isBuffer(data) || typeof data === 'string') {
|
|
91
|
+
// Use neural import for parsing and analysis
|
|
92
|
+
const analysis = await this.neuralImport.getNeuralAnalysis(data, format);
|
|
93
|
+
// Extract items and relationships
|
|
94
|
+
items = analysis.detectedEntities.map(entity => ({
|
|
95
|
+
data: entity.originalData,
|
|
96
|
+
type: entity.nounType,
|
|
97
|
+
confidence: entity.confidence,
|
|
98
|
+
id: entity.suggestedId
|
|
99
|
+
}));
|
|
100
|
+
if (options.extractRelationships !== false) {
|
|
101
|
+
relationships = analysis.detectedRelationships;
|
|
102
|
+
}
|
|
103
|
+
// Log insights
|
|
104
|
+
for (const insight of analysis.insights) {
|
|
105
|
+
prodLog.info(`🧠 ${insight.description} (confidence: ${insight.confidence})`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (Array.isArray(data)) {
|
|
109
|
+
items = data;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
items = [data];
|
|
113
|
+
}
|
|
114
|
+
result.stats.total = items.length;
|
|
115
|
+
// Import items in batches
|
|
116
|
+
const batchSize = options.batchSize || 50;
|
|
117
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
118
|
+
const batch = items.slice(i, i + batchSize);
|
|
119
|
+
// Process batch in parallel if enabled
|
|
120
|
+
const promises = batch.map(async (item) => {
|
|
121
|
+
try {
|
|
122
|
+
// Detect type if needed
|
|
123
|
+
let nounType = item.type || options.typeHint;
|
|
124
|
+
if (!nounType && options.autoDetect !== false && this.typeMatcher) {
|
|
125
|
+
const match = await this.typeMatcher.matchNounType(item.data || item);
|
|
126
|
+
nounType = match.type;
|
|
127
|
+
}
|
|
128
|
+
// Prepare the data to import
|
|
129
|
+
const dataToImport = item.data || item;
|
|
130
|
+
// Create metadata combining original data with import metadata
|
|
131
|
+
const metadata = {
|
|
132
|
+
...(typeof dataToImport === 'object' ? dataToImport : {}),
|
|
133
|
+
...(item.data?.metadata || {}),
|
|
134
|
+
nounType,
|
|
135
|
+
_importedAt: new Date().toISOString(),
|
|
136
|
+
_confidence: item.confidence
|
|
137
|
+
};
|
|
138
|
+
// Add to brain - pass object once, it becomes both vector source and metadata
|
|
139
|
+
const id = await this.brain.addNoun(metadata);
|
|
140
|
+
result.nouns.push(id);
|
|
141
|
+
result.stats.imported++;
|
|
142
|
+
return id;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
result.errors.push(`Failed to import item: ${error.message}`);
|
|
146
|
+
result.stats.failed++;
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
if (options.parallel !== false) {
|
|
151
|
+
await Promise.all(promises);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
for (const promise of promises) {
|
|
155
|
+
await promise;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Import relationships
|
|
160
|
+
for (const rel of relationships) {
|
|
161
|
+
try {
|
|
162
|
+
// Match verb type if needed
|
|
163
|
+
let verbType = rel.verbType;
|
|
164
|
+
if (!Object.values(VerbType).includes(verbType) && this.typeMatcher) {
|
|
165
|
+
const match = await this.typeMatcher.matchVerbType({ id: rel.sourceId }, { id: rel.targetId }, rel.verbType);
|
|
166
|
+
verbType = match.type;
|
|
167
|
+
}
|
|
168
|
+
const verbId = await this.brain.addVerb(rel.sourceId, rel.targetId, verbType, rel.metadata, rel.weight);
|
|
169
|
+
result.verbs.push(verbId);
|
|
170
|
+
result.stats.relationships++;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
result.errors.push(`Failed to create relationship: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
result.success = result.stats.imported > 0;
|
|
177
|
+
prodLog.info(`✨ Import complete: ${result.stats.imported}/${result.stats.total} items, ${result.stats.relationships} relationships`);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
result.errors.push(`Import failed: ${error.message}`);
|
|
181
|
+
prodLog.error('Import failed:', error);
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Import from file
|
|
187
|
+
*/
|
|
188
|
+
async importFile(filePath, options = {}) {
|
|
189
|
+
return this.import(filePath, { ...options, source: 'file' });
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Import from URL
|
|
193
|
+
*/
|
|
194
|
+
async importUrl(url, options = {}) {
|
|
195
|
+
return this.import(url, { ...options, source: 'url' });
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Detect source type
|
|
199
|
+
*/
|
|
200
|
+
async detectSourceType(source, hint) {
|
|
201
|
+
if (hint && hint !== 'auto') {
|
|
202
|
+
return hint;
|
|
203
|
+
}
|
|
204
|
+
if (typeof source === 'string') {
|
|
205
|
+
// Check if URL
|
|
206
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
207
|
+
return 'url';
|
|
208
|
+
}
|
|
209
|
+
// Check if file path exists
|
|
210
|
+
try {
|
|
211
|
+
if (await fs.exists(source)) {
|
|
212
|
+
return 'file';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch { }
|
|
216
|
+
}
|
|
217
|
+
return 'data';
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Detect format from file path
|
|
221
|
+
*/
|
|
222
|
+
detectFormatFromPath(filePath) {
|
|
223
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
224
|
+
switch (ext) {
|
|
225
|
+
case '.json': return 'json';
|
|
226
|
+
case '.csv': return 'csv';
|
|
227
|
+
case '.txt': return 'text';
|
|
228
|
+
case '.md': return 'text';
|
|
229
|
+
case '.yaml':
|
|
230
|
+
case '.yml': return 'yaml';
|
|
231
|
+
default: return 'auto';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Read file
|
|
236
|
+
*/
|
|
237
|
+
async readFile(filePath) {
|
|
238
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
239
|
+
return Buffer.from(content, 'utf8');
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Fetch from URL
|
|
243
|
+
*/
|
|
244
|
+
async fetchFromUrl(url) {
|
|
245
|
+
const response = await fetch(url);
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
248
|
+
}
|
|
249
|
+
return response.text();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Create an import manager instance
|
|
254
|
+
*/
|
|
255
|
+
export function createImportManager(brain) {
|
|
256
|
+
return new ImportManager(brain);
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=importManager.js.map
|
|
@@ -83,6 +83,10 @@ export declare class TripleIntelligenceEngine {
|
|
|
83
83
|
* Field-based filtering
|
|
84
84
|
*/
|
|
85
85
|
private fieldFilter;
|
|
86
|
+
/**
|
|
87
|
+
* Fallback manual metadata filtering when index is not available
|
|
88
|
+
*/
|
|
89
|
+
private manualMetadataFilter;
|
|
86
90
|
/**
|
|
87
91
|
* Fusion ranking combines all signals
|
|
88
92
|
*/
|
|
@@ -171,25 +171,30 @@ export class TripleIntelligenceEngine {
|
|
|
171
171
|
}
|
|
172
172
|
break;
|
|
173
173
|
case 'vector':
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
// CRITICAL: If we have a previous step that returned 0 candidates,
|
|
175
|
+
// we must respect that and not do a fresh search
|
|
176
|
+
if (candidates.length === 0 && plan.steps[0].type === 'vector') {
|
|
177
|
+
// This is the first step - do initial vector search
|
|
176
178
|
const results = await this.vectorSearch(query.like || query.similar, query.limit);
|
|
177
179
|
candidates = results;
|
|
178
180
|
}
|
|
179
|
-
else {
|
|
180
|
-
// Vector search within candidates
|
|
181
|
+
else if (candidates.length > 0) {
|
|
182
|
+
// Vector search within existing candidates
|
|
181
183
|
candidates = await this.vectorSearchWithin(query.like || query.similar, candidates);
|
|
182
184
|
}
|
|
185
|
+
// If candidates.length === 0 and this isn't the first step, keep empty candidates
|
|
183
186
|
break;
|
|
184
187
|
case 'graph':
|
|
185
|
-
|
|
186
|
-
|
|
188
|
+
// CRITICAL: Same logic as vector - respect empty candidates from previous steps
|
|
189
|
+
if (candidates.length === 0 && plan.steps[0].type === 'graph') {
|
|
190
|
+
// This is the first step - do initial graph traversal
|
|
187
191
|
candidates = await this.graphTraversal(query.connected);
|
|
188
192
|
}
|
|
189
|
-
else {
|
|
190
|
-
// Graph expansion from candidates
|
|
193
|
+
else if (candidates.length > 0) {
|
|
194
|
+
// Graph expansion from existing candidates
|
|
191
195
|
candidates = await this.graphExpand(candidates, query.connected);
|
|
192
196
|
}
|
|
197
|
+
// If candidates.length === 0 and this isn't the first step, keep empty candidates
|
|
193
198
|
break;
|
|
194
199
|
case 'fusion':
|
|
195
200
|
// Final fusion ranking
|
|
@@ -248,7 +253,13 @@ export class TripleIntelligenceEngine {
|
|
|
248
253
|
// Use the MetadataIndex directly for FAST field queries!
|
|
249
254
|
// This uses B-tree indexes for O(log n) range queries
|
|
250
255
|
// and hash indexes for O(1) exact matches
|
|
251
|
-
const
|
|
256
|
+
const metadataIndex = this.brain.metadataIndex;
|
|
257
|
+
// Check if metadata index is properly initialized
|
|
258
|
+
if (!metadataIndex || typeof metadataIndex.getIdsForFilter !== 'function') {
|
|
259
|
+
// Fallback to manual filtering - slower but works
|
|
260
|
+
return this.manualMetadataFilter(where);
|
|
261
|
+
}
|
|
262
|
+
const matchingIds = await metadataIndex.getIdsForFilter(where) || [];
|
|
252
263
|
// Convert to result format with metadata
|
|
253
264
|
const results = [];
|
|
254
265
|
for (const id of matchingIds.slice(0, 1000)) {
|
|
@@ -263,6 +274,25 @@ export class TripleIntelligenceEngine {
|
|
|
263
274
|
}
|
|
264
275
|
return results;
|
|
265
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Fallback manual metadata filtering when index is not available
|
|
279
|
+
*/
|
|
280
|
+
async manualMetadataFilter(where) {
|
|
281
|
+
const { matchesMetadataFilter } = await import('../utils/metadataFilter.js');
|
|
282
|
+
const results = [];
|
|
283
|
+
// Get all nouns and manually filter them
|
|
284
|
+
const allNouns = this.brain.index.getNouns();
|
|
285
|
+
for (const [id, noun] of Array.from(allNouns.entries()).slice(0, 1000)) {
|
|
286
|
+
if (noun && matchesMetadataFilter(noun.metadata || {}, where)) {
|
|
287
|
+
results.push({
|
|
288
|
+
id,
|
|
289
|
+
score: 1.0,
|
|
290
|
+
metadata: noun.metadata || {}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return results;
|
|
295
|
+
}
|
|
266
296
|
/**
|
|
267
297
|
* Fusion ranking combines all signals
|
|
268
298
|
*/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedicated index for tracking soft-deleted items
|
|
3
|
+
* This is MUCH more efficient than checking every item in the database
|
|
4
|
+
*
|
|
5
|
+
* Performance characteristics:
|
|
6
|
+
* - Add deleted item: O(1)
|
|
7
|
+
* - Remove deleted item: O(1)
|
|
8
|
+
* - Check if deleted: O(1)
|
|
9
|
+
* - Get all deleted: O(d) where d = number of deleted items << total items
|
|
10
|
+
*/
|
|
11
|
+
export declare class DeletedItemsIndex {
|
|
12
|
+
private deletedIds;
|
|
13
|
+
private deletedCount;
|
|
14
|
+
/**
|
|
15
|
+
* Mark an item as deleted
|
|
16
|
+
*/
|
|
17
|
+
markDeleted(id: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Mark an item as not deleted (restored)
|
|
20
|
+
*/
|
|
21
|
+
markRestored(id: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Check if an item is deleted - O(1)
|
|
24
|
+
*/
|
|
25
|
+
isDeleted(id: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Get all deleted item IDs - O(d)
|
|
28
|
+
*/
|
|
29
|
+
getAllDeleted(): string[];
|
|
30
|
+
/**
|
|
31
|
+
* Filter out deleted items from results - O(k) where k = result count
|
|
32
|
+
*/
|
|
33
|
+
filterDeleted<T extends {
|
|
34
|
+
id?: string;
|
|
35
|
+
}>(items: T[]): T[];
|
|
36
|
+
/**
|
|
37
|
+
* Get statistics
|
|
38
|
+
*/
|
|
39
|
+
getStats(): {
|
|
40
|
+
deletedCount: number;
|
|
41
|
+
memoryUsage: number;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Clear all deleted items (for testing)
|
|
45
|
+
*/
|
|
46
|
+
clear(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Serialize for persistence
|
|
49
|
+
*/
|
|
50
|
+
serialize(): string;
|
|
51
|
+
/**
|
|
52
|
+
* Deserialize from persistence
|
|
53
|
+
*/
|
|
54
|
+
deserialize(data: string): void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Global singleton for deleted items tracking
|
|
58
|
+
*/
|
|
59
|
+
export declare const deletedItemsIndex: DeletedItemsIndex;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedicated index for tracking soft-deleted items
|
|
3
|
+
* This is MUCH more efficient than checking every item in the database
|
|
4
|
+
*
|
|
5
|
+
* Performance characteristics:
|
|
6
|
+
* - Add deleted item: O(1)
|
|
7
|
+
* - Remove deleted item: O(1)
|
|
8
|
+
* - Check if deleted: O(1)
|
|
9
|
+
* - Get all deleted: O(d) where d = number of deleted items << total items
|
|
10
|
+
*/
|
|
11
|
+
export class DeletedItemsIndex {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.deletedIds = new Set();
|
|
14
|
+
this.deletedCount = 0;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Mark an item as deleted
|
|
18
|
+
*/
|
|
19
|
+
markDeleted(id) {
|
|
20
|
+
if (!this.deletedIds.has(id)) {
|
|
21
|
+
this.deletedIds.add(id);
|
|
22
|
+
this.deletedCount++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mark an item as not deleted (restored)
|
|
27
|
+
*/
|
|
28
|
+
markRestored(id) {
|
|
29
|
+
if (this.deletedIds.delete(id)) {
|
|
30
|
+
this.deletedCount--;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check if an item is deleted - O(1)
|
|
35
|
+
*/
|
|
36
|
+
isDeleted(id) {
|
|
37
|
+
return this.deletedIds.has(id);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get all deleted item IDs - O(d)
|
|
41
|
+
*/
|
|
42
|
+
getAllDeleted() {
|
|
43
|
+
return Array.from(this.deletedIds);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Filter out deleted items from results - O(k) where k = result count
|
|
47
|
+
*/
|
|
48
|
+
filterDeleted(items) {
|
|
49
|
+
if (this.deletedCount === 0) {
|
|
50
|
+
// Fast path - no deleted items
|
|
51
|
+
return items;
|
|
52
|
+
}
|
|
53
|
+
return items.filter(item => {
|
|
54
|
+
const id = item.id;
|
|
55
|
+
return id ? !this.deletedIds.has(id) : true;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get statistics
|
|
60
|
+
*/
|
|
61
|
+
getStats() {
|
|
62
|
+
return {
|
|
63
|
+
deletedCount: this.deletedCount,
|
|
64
|
+
memoryUsage: this.deletedCount * 100 // Rough estimate: 100 bytes per ID
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Clear all deleted items (for testing)
|
|
69
|
+
*/
|
|
70
|
+
clear() {
|
|
71
|
+
this.deletedIds.clear();
|
|
72
|
+
this.deletedCount = 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Serialize for persistence
|
|
76
|
+
*/
|
|
77
|
+
serialize() {
|
|
78
|
+
return JSON.stringify(Array.from(this.deletedIds));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Deserialize from persistence
|
|
82
|
+
*/
|
|
83
|
+
deserialize(data) {
|
|
84
|
+
try {
|
|
85
|
+
const ids = JSON.parse(data);
|
|
86
|
+
this.deletedIds = new Set(ids);
|
|
87
|
+
this.deletedCount = this.deletedIds.size;
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.warn('Failed to deserialize deleted items index');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Global singleton for deleted items tracking
|
|
96
|
+
*/
|
|
97
|
+
export const deletedItemsIndex = new DeletedItemsIndex();
|
|
98
|
+
//# sourceMappingURL=deletedItemsIndex.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility to ensure all metadata has the deleted field set properly
|
|
3
|
+
* This is CRITICAL for O(1) soft delete filtering performance
|
|
4
|
+
*
|
|
5
|
+
* Uses _brainy namespace to avoid conflicts with user metadata
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Ensure metadata has internal Brainy fields set
|
|
9
|
+
* @param metadata The metadata object (could be null/undefined)
|
|
10
|
+
* @param preserveExisting If true, preserve existing deleted value
|
|
11
|
+
* @returns Metadata with internal fields guaranteed
|
|
12
|
+
*/
|
|
13
|
+
export declare function ensureDeletedField(metadata: any, preserveExisting?: boolean): any;
|
|
14
|
+
/**
|
|
15
|
+
* Mark an item as soft deleted
|
|
16
|
+
* @param metadata The metadata object
|
|
17
|
+
* @returns Metadata with _brainy.deleted=true
|
|
18
|
+
*/
|
|
19
|
+
export declare function markAsDeleted(metadata: any): any;
|
|
20
|
+
/**
|
|
21
|
+
* Mark an item as restored (not deleted)
|
|
22
|
+
* @param metadata The metadata object
|
|
23
|
+
* @returns Metadata with _brainy.deleted=false
|
|
24
|
+
*/
|
|
25
|
+
export declare function markAsRestored(metadata: any): any;
|
|
26
|
+
/**
|
|
27
|
+
* Check if an item is deleted
|
|
28
|
+
* @param metadata The metadata object
|
|
29
|
+
* @returns true if deleted, false otherwise (including if field missing)
|
|
30
|
+
*/
|
|
31
|
+
export declare function isDeleted(metadata: any): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Check if an item is active (not deleted)
|
|
34
|
+
* @param metadata The metadata object
|
|
35
|
+
* @returns true if not deleted (default), false if deleted
|
|
36
|
+
*/
|
|
37
|
+
export declare function isActive(metadata: any): boolean;
|
|
38
|
+
export declare const BRAINY_DELETED_FIELD = "_brainy.deleted";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility to ensure all metadata has the deleted field set properly
|
|
3
|
+
* This is CRITICAL for O(1) soft delete filtering performance
|
|
4
|
+
*
|
|
5
|
+
* Uses _brainy namespace to avoid conflicts with user metadata
|
|
6
|
+
*/
|
|
7
|
+
const BRAINY_NAMESPACE = '_brainy';
|
|
8
|
+
/**
|
|
9
|
+
* Ensure metadata has internal Brainy fields set
|
|
10
|
+
* @param metadata The metadata object (could be null/undefined)
|
|
11
|
+
* @param preserveExisting If true, preserve existing deleted value
|
|
12
|
+
* @returns Metadata with internal fields guaranteed
|
|
13
|
+
*/
|
|
14
|
+
export function ensureDeletedField(metadata, preserveExisting = true) {
|
|
15
|
+
// Handle null/undefined metadata
|
|
16
|
+
if (!metadata) {
|
|
17
|
+
return {
|
|
18
|
+
[BRAINY_NAMESPACE]: {
|
|
19
|
+
deleted: false,
|
|
20
|
+
version: 1
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Clone to avoid mutation
|
|
25
|
+
const result = { ...metadata };
|
|
26
|
+
// Ensure _brainy namespace exists
|
|
27
|
+
if (!result[BRAINY_NAMESPACE]) {
|
|
28
|
+
result[BRAINY_NAMESPACE] = {};
|
|
29
|
+
}
|
|
30
|
+
// Set deleted field if not present
|
|
31
|
+
if (!('deleted' in result[BRAINY_NAMESPACE])) {
|
|
32
|
+
result[BRAINY_NAMESPACE].deleted = false;
|
|
33
|
+
}
|
|
34
|
+
else if (!preserveExisting) {
|
|
35
|
+
// Force to false if not preserving
|
|
36
|
+
result[BRAINY_NAMESPACE].deleted = false;
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Mark an item as soft deleted
|
|
42
|
+
* @param metadata The metadata object
|
|
43
|
+
* @returns Metadata with _brainy.deleted=true
|
|
44
|
+
*/
|
|
45
|
+
export function markAsDeleted(metadata) {
|
|
46
|
+
const result = ensureDeletedField(metadata);
|
|
47
|
+
result[BRAINY_NAMESPACE].deleted = true;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Mark an item as restored (not deleted)
|
|
52
|
+
* @param metadata The metadata object
|
|
53
|
+
* @returns Metadata with _brainy.deleted=false
|
|
54
|
+
*/
|
|
55
|
+
export function markAsRestored(metadata) {
|
|
56
|
+
const result = ensureDeletedField(metadata);
|
|
57
|
+
result[BRAINY_NAMESPACE].deleted = false;
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if an item is deleted
|
|
62
|
+
* @param metadata The metadata object
|
|
63
|
+
* @returns true if deleted, false otherwise (including if field missing)
|
|
64
|
+
*/
|
|
65
|
+
export function isDeleted(metadata) {
|
|
66
|
+
return metadata?.[BRAINY_NAMESPACE]?.deleted === true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if an item is active (not deleted)
|
|
70
|
+
* @param metadata The metadata object
|
|
71
|
+
* @returns true if not deleted (default), false if deleted
|
|
72
|
+
*/
|
|
73
|
+
export function isActive(metadata) {
|
|
74
|
+
// If no deleted field or deleted=false, item is active
|
|
75
|
+
return !isDeleted(metadata);
|
|
76
|
+
}
|
|
77
|
+
// Export the namespace constant for use in queries
|
|
78
|
+
export const BRAINY_DELETED_FIELD = `${BRAINY_NAMESPACE}.deleted`;
|
|
79
|
+
//# sourceMappingURL=ensureDeleted.js.map
|
|
@@ -24,8 +24,13 @@ function matchesQuery(value, query) {
|
|
|
24
24
|
case 'notEquals':
|
|
25
25
|
case 'isNot':
|
|
26
26
|
case 'ne':
|
|
27
|
+
// Special handling: if value is undefined and operand is not undefined,
|
|
28
|
+
// they are not equal (so the condition passes)
|
|
29
|
+
// This ensures items without a 'deleted' field match 'deleted !== true'
|
|
27
30
|
if (value === operand)
|
|
28
31
|
return false;
|
|
32
|
+
// If value is undefined and operand is not, they're not equal (pass)
|
|
33
|
+
// If both are undefined, they're equal (fail, handled above)
|
|
29
34
|
break;
|
|
30
35
|
// Comparison operators
|
|
31
36
|
case 'greaterThan':
|
|
@@ -107,6 +107,10 @@ export declare class MetadataIndexManager {
|
|
|
107
107
|
* Remove item from metadata indexes
|
|
108
108
|
*/
|
|
109
109
|
removeFromIndex(id: string, metadata?: any): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Get all IDs in the index
|
|
112
|
+
*/
|
|
113
|
+
getAllIds(): Promise<string[]>;
|
|
110
114
|
/**
|
|
111
115
|
* Get IDs for a specific field-value combination with caching
|
|
112
116
|
*/
|