mindoodb 0.0.1
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 +263 -0
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts +5 -0
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts.map +1 -0
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js +17 -0
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js.map +1 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +111 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +237 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts +78 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -0
- package/dist/appendonlystores/network/HttpTransport.js +356 -0
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +131 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +248 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -0
- package/dist/appendonlystores/network/index.d.ts +8 -0
- package/dist/appendonlystores/network/index.d.ts.map +1 -0
- package/dist/appendonlystores/network/index.js +21 -0
- package/dist/appendonlystores/network/index.js.map +1 -0
- package/dist/browser/crypto/BrowserCryptoAdapter.d.ts +14 -0
- package/dist/browser/crypto/BrowserCryptoAdapter.d.ts.map +1 -0
- package/dist/browser/crypto/BrowserCryptoAdapter.js +30 -0
- package/dist/browser/crypto/BrowserCryptoAdapter.js.map +1 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +26 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/core/BaseMindooDB.d.ts +182 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -0
- package/dist/core/BaseMindooDB.js +1701 -0
- package/dist/core/BaseMindooDB.js.map +1 -0
- package/dist/core/BaseMindooTenant.d.ts +155 -0
- package/dist/core/BaseMindooTenant.d.ts.map +1 -0
- package/dist/core/BaseMindooTenant.js +673 -0
- package/dist/core/BaseMindooTenant.js.map +1 -0
- package/dist/core/BaseMindooTenantDirectory.d.ts +99 -0
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -0
- package/dist/core/BaseMindooTenantDirectory.js +776 -0
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -0
- package/dist/core/BaseMindooTenantFactory.d.ts +91 -0
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -0
- package/dist/core/BaseMindooTenantFactory.js +297 -0
- package/dist/core/BaseMindooTenantFactory.js.map +1 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +111 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +293 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +78 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -0
- package/dist/core/appendonlystores/network/AuthenticationService.js +297 -0
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -0
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +174 -0
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -0
- package/dist/core/appendonlystores/network/NetworkTransport.js +3 -0
- package/dist/core/appendonlystores/network/NetworkTransport.js.map +1 -0
- package/dist/core/appendonlystores/network/types.d.ts +135 -0
- package/dist/core/appendonlystores/network/types.d.ts.map +1 -0
- package/dist/core/appendonlystores/network/types.js +35 -0
- package/dist/core/appendonlystores/network/types.js.map +1 -0
- package/dist/core/appendonlystores/types.d.ts +157 -0
- package/dist/core/appendonlystores/types.d.ts.map +1 -0
- package/dist/core/appendonlystores/types.js +3 -0
- package/dist/core/appendonlystores/types.js.map +1 -0
- package/dist/core/crypto/CryptoAdapter.d.ts +19 -0
- package/dist/core/crypto/CryptoAdapter.d.ts.map +1 -0
- package/dist/core/crypto/CryptoAdapter.js +3 -0
- package/dist/core/crypto/CryptoAdapter.js.map +1 -0
- package/dist/core/crypto/MindooDocSigner.d.ts +106 -0
- package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -0
- package/dist/core/crypto/MindooDocSigner.js +192 -0
- package/dist/core/crypto/MindooDocSigner.js.map +1 -0
- package/dist/core/crypto/RSAEncryption.d.ts +62 -0
- package/dist/core/crypto/RSAEncryption.d.ts.map +1 -0
- package/dist/core/crypto/RSAEncryption.js +167 -0
- package/dist/core/crypto/RSAEncryption.js.map +1 -0
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +17 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +53 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.d.ts +33 -0
- package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.js +27 -0
- package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.js.map +1 -0
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts +31 -0
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.js +3 -0
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.js.map +1 -0
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +52 -0
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +106 -0
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts +44 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.js +107 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.js.map +1 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts +28 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js +133 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualView.d.ts +109 -0
- package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualView.js +504 -0
- package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts +59 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.js +70 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts +49 -0
- package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewDataChange.js +69 -0
- package/dist/core/indexing/virtualviews/VirtualViewDataChange.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +121 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +365 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts +131 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.js +236 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts +330 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.js +1010 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.js.map +1 -0
- package/dist/core/indexing/virtualviews/index.d.ts +53 -0
- package/dist/core/indexing/virtualviews/index.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/index.js +89 -0
- package/dist/core/indexing/virtualviews/index.js.map +1 -0
- package/dist/core/indexing/virtualviews/types.d.ts +115 -0
- package/dist/core/indexing/virtualviews/types.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/types.js +175 -0
- package/dist/core/indexing/virtualviews/types.js.map +1 -0
- package/dist/core/keys/KeyBag.d.ts +122 -0
- package/dist/core/keys/KeyBag.d.ts.map +1 -0
- package/dist/core/keys/KeyBag.js +401 -0
- package/dist/core/keys/KeyBag.js.map +1 -0
- package/dist/core/types.d.ts +1004 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/userid/index.d.ts +53 -0
- package/dist/core/userid/index.d.ts.map +1 -0
- package/dist/core/userid/index.js +3 -0
- package/dist/core/userid/index.js.map +1 -0
- package/dist/core/utils/idGeneration.d.ts +109 -0
- package/dist/core/utils/idGeneration.d.ts.map +1 -0
- package/dist/core/utils/idGeneration.js +182 -0
- package/dist/core/utils/idGeneration.js.map +1 -0
- package/dist/core/utils/index.d.ts +5 -0
- package/dist/core/utils/index.d.ts.map +1 -0
- package/dist/core/utils/index.js +19 -0
- package/dist/core/utils/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/node/crypto/NodeCryptoAdapter.d.ts +16 -0
- package/dist/node/crypto/NodeCryptoAdapter.d.ts.map +1 -0
- package/dist/node/crypto/NodeCryptoAdapter.js +53 -0
- package/dist/node/crypto/NodeCryptoAdapter.js.map +1 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +26 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1701 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.BaseMindooDB = void 0;
|
|
37
|
+
const Automerge = __importStar(require("@automerge/automerge"));
|
|
38
|
+
const uuid_1 = require("uuid");
|
|
39
|
+
const idGeneration_1 = require("./utils/idGeneration");
|
|
40
|
+
const errors_1 = require("./errors");
|
|
41
|
+
/**
|
|
42
|
+
* Default chunk size for attachments: 256KB
|
|
43
|
+
*/
|
|
44
|
+
const DEFAULT_CHUNK_SIZE_BYTES = 256 * 1024;
|
|
45
|
+
/**
|
|
46
|
+
* BaseMindooDB is a platform-agnostic implementation of MindooDB
|
|
47
|
+
* that works in both browser and server environments.
|
|
48
|
+
*
|
|
49
|
+
* It receives MindooTenant and ContentAddressedStore in the constructor,
|
|
50
|
+
* allowing platform-specific implementations of those interfaces.
|
|
51
|
+
*
|
|
52
|
+
* Dependencies:
|
|
53
|
+
* - @automerge/automerge: For CRDT document management
|
|
54
|
+
* - Web Crypto API: Available in both browser (window.crypto) and Node.js (crypto)
|
|
55
|
+
* - Ed25519 signing/verification (Node.js 15+, Chrome 92+)
|
|
56
|
+
* - AES-256-GCM encryption (widely supported)
|
|
57
|
+
*
|
|
58
|
+
* TODO: Verify Automerge 2.x API methods:
|
|
59
|
+
* - Automerge.getChangeHash(change) - verify method name
|
|
60
|
+
* - Automerge.getHeads(doc) - verify method name
|
|
61
|
+
* - Automerge.load(bytes) - verify method name for snapshots
|
|
62
|
+
* - Automerge.applyChanges(doc, changes) - verify method signature
|
|
63
|
+
*/
|
|
64
|
+
class BaseMindooDB {
|
|
65
|
+
constructor(tenant, store, attachmentStore, attachmentConfig, adminOnlyDb = false) {
|
|
66
|
+
// Internal index: sorted array of document entries, maintained in order by (lastModified, docId)
|
|
67
|
+
// This allows efficient incremental processing without sorting on each call
|
|
68
|
+
this.index = [];
|
|
69
|
+
// Lookup map for O(1) access to index entries by docId
|
|
70
|
+
this.indexLookup = new Map(); // Map<docId, arrayIndex>
|
|
71
|
+
// Cache of loaded documents: Map<docId, InternalDoc>
|
|
72
|
+
this.docCache = new Map();
|
|
73
|
+
// Track which entry IDs we've already processed
|
|
74
|
+
this.processedEntryIds = [];
|
|
75
|
+
// Index: automergeHash -> entryId for each document
|
|
76
|
+
// Used for resolving Automerge dependency hashes to entry IDs
|
|
77
|
+
this.automergeHashToEntryId = new Map(); // Map<docId, Map<automergeHash, entryId>>
|
|
78
|
+
this.tenant = tenant;
|
|
79
|
+
this.store = store;
|
|
80
|
+
this.attachmentStore = attachmentStore;
|
|
81
|
+
this.chunkSizeBytes = attachmentConfig?.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE_BYTES;
|
|
82
|
+
this._isAdminOnlyDb = adminOnlyDb;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get the admin public key from the tenant.
|
|
86
|
+
* Only used when adminOnlyDb is true.
|
|
87
|
+
*/
|
|
88
|
+
getAdminPublicKey() {
|
|
89
|
+
return this.tenant.getAdministrationPublicKey();
|
|
90
|
+
}
|
|
91
|
+
isAdminOnlyDb() {
|
|
92
|
+
return this._isAdminOnlyDb;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Compare two index entries for sorting.
|
|
96
|
+
* Returns negative if a < b, positive if a > b, 0 if equal.
|
|
97
|
+
* Sorts by lastModified first, then by docId for uniqueness.
|
|
98
|
+
*/
|
|
99
|
+
compareIndexEntries(a, b) {
|
|
100
|
+
if (a.lastModified !== b.lastModified) {
|
|
101
|
+
return a.lastModified - b.lastModified;
|
|
102
|
+
}
|
|
103
|
+
return a.docId.localeCompare(b.docId);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Update the index entry for a document.
|
|
107
|
+
* When a document changes, it's removed from its current position and inserted
|
|
108
|
+
* at the correct sorted position to maintain order by (lastModified, docId).
|
|
109
|
+
*
|
|
110
|
+
* @param docId The document ID
|
|
111
|
+
* @param lastModified The new last modified timestamp
|
|
112
|
+
* @param isDeleted Whether the document is deleted
|
|
113
|
+
*/
|
|
114
|
+
updateIndex(docId, lastModified, isDeleted) {
|
|
115
|
+
// Remove existing entry if present
|
|
116
|
+
const existingIndex = this.indexLookup.get(docId);
|
|
117
|
+
if (existingIndex !== undefined) {
|
|
118
|
+
// Remove from array
|
|
119
|
+
this.index.splice(existingIndex, 1);
|
|
120
|
+
// Update lookup map for all entries after the removed one
|
|
121
|
+
for (let i = existingIndex; i < this.index.length; i++) {
|
|
122
|
+
this.indexLookup.set(this.index[i].docId, i);
|
|
123
|
+
}
|
|
124
|
+
// Remove the entry from lookup
|
|
125
|
+
this.indexLookup.delete(docId);
|
|
126
|
+
}
|
|
127
|
+
// Find insertion point using binary search to maintain sorted order
|
|
128
|
+
const newEntry = { docId, lastModified, isDeleted };
|
|
129
|
+
let insertIndex = this.index.length; // Default to end
|
|
130
|
+
// Binary search for insertion point
|
|
131
|
+
let left = 0;
|
|
132
|
+
let right = this.index.length - 1;
|
|
133
|
+
while (left <= right) {
|
|
134
|
+
const mid = Math.floor((left + right) / 2);
|
|
135
|
+
const cmp = this.compareIndexEntries(newEntry, this.index[mid]);
|
|
136
|
+
if (cmp < 0) {
|
|
137
|
+
right = mid - 1;
|
|
138
|
+
insertIndex = mid;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
left = mid + 1;
|
|
142
|
+
insertIndex = mid + 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Insert at the correct position
|
|
146
|
+
this.index.splice(insertIndex, 0, newEntry);
|
|
147
|
+
// Update lookup map for all entries from insertion point onwards
|
|
148
|
+
for (let i = insertIndex; i < this.index.length; i++) {
|
|
149
|
+
this.indexLookup.set(this.index[i].docId, i);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Register an automerge hash -> entry ID mapping for a document.
|
|
154
|
+
* This is used to resolve Automerge dependency hashes to entry IDs.
|
|
155
|
+
*/
|
|
156
|
+
registerAutomergeHashMapping(docId, automergeHash, entryId) {
|
|
157
|
+
if (!this.automergeHashToEntryId.has(docId)) {
|
|
158
|
+
this.automergeHashToEntryId.set(docId, new Map());
|
|
159
|
+
}
|
|
160
|
+
this.automergeHashToEntryId.get(docId).set(automergeHash, entryId);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get the entry ID for an automerge hash within a document.
|
|
164
|
+
* Returns null if not found.
|
|
165
|
+
*/
|
|
166
|
+
getEntryIdForAutomergeHash(docId, automergeHash) {
|
|
167
|
+
const docIndex = this.automergeHashToEntryId.get(docId);
|
|
168
|
+
if (docIndex) {
|
|
169
|
+
return docIndex.get(automergeHash) || null;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Resolve Automerge dependency hashes to entry IDs for a document.
|
|
175
|
+
* Returns the entry IDs for the given automerge hashes.
|
|
176
|
+
*/
|
|
177
|
+
resolveAutomergeDepsToEntryIds(docId, automergeHashes) {
|
|
178
|
+
const entryIds = [];
|
|
179
|
+
for (const hash of automergeHashes) {
|
|
180
|
+
const entryId = this.getEntryIdForAutomergeHash(docId, hash);
|
|
181
|
+
if (entryId) {
|
|
182
|
+
entryIds.push(entryId);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.warn(`[BaseMindooDB] Could not resolve automerge hash ${hash} to entry ID for doc ${docId}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return entryIds;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get the SubtleCrypto instance from the tenant's crypto adapter.
|
|
192
|
+
*/
|
|
193
|
+
getSubtle() {
|
|
194
|
+
return this.tenant.getCryptoAdapter().getSubtle();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Initialize the database instance.
|
|
198
|
+
*/
|
|
199
|
+
async initialize() {
|
|
200
|
+
console.log(`[BaseMindooDB] Initializing database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
|
|
201
|
+
await this.syncStoreChanges();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Sync changes from the content-addressed store by finding new entries and processing them.
|
|
205
|
+
* This method can be called multiple times to incrementally sync new entries.
|
|
206
|
+
* On first call (when processedEntryIds is empty), it will process all entries.
|
|
207
|
+
*/
|
|
208
|
+
async syncStoreChanges() {
|
|
209
|
+
console.log(`[BaseMindooDB] Syncing store changes for database ${this.store.getId()} in tenant ${this.tenant.getId()}`);
|
|
210
|
+
console.log(`[BaseMindooDB] Already processed ${this.processedEntryIds.length} entry IDs`);
|
|
211
|
+
// Find new entries that we haven't processed yet
|
|
212
|
+
const newEntryMetadata = await this.store.findNewEntries(this.processedEntryIds);
|
|
213
|
+
console.log(`[BaseMindooDB] Found ${newEntryMetadata.length} new entries`);
|
|
214
|
+
if (newEntryMetadata.length === 0) {
|
|
215
|
+
console.log(`[BaseMindooDB] No new entries to process`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Group new entries by document ID
|
|
219
|
+
const entriesByDoc = new Map();
|
|
220
|
+
for (const entryMeta of newEntryMetadata) {
|
|
221
|
+
if (!entriesByDoc.has(entryMeta.docId)) {
|
|
222
|
+
entriesByDoc.set(entryMeta.docId, []);
|
|
223
|
+
}
|
|
224
|
+
entriesByDoc.get(entryMeta.docId).push(entryMeta);
|
|
225
|
+
}
|
|
226
|
+
// Process each document with new entries
|
|
227
|
+
// Reload documents to apply all changes (including new ones)
|
|
228
|
+
console.log(`[BaseMindooDB] Processing ${entriesByDoc.size} documents with new entries`);
|
|
229
|
+
for (const [docId, entryMetadataList] of entriesByDoc) {
|
|
230
|
+
try {
|
|
231
|
+
console.log(`[BaseMindooDB] ===== Processing document ${docId} with ${entryMetadataList.length} new entry(s) in syncStoreChanges =====`);
|
|
232
|
+
// Clear cache for this document so it gets reloaded with all entries
|
|
233
|
+
this.docCache.delete(docId);
|
|
234
|
+
console.log(`[BaseMindooDB] Cleared cache for document ${docId}`);
|
|
235
|
+
// Reload document (this will load all entries including new ones)
|
|
236
|
+
console.log(`[BaseMindooDB] About to call loadDocumentInternal for document ${docId}`);
|
|
237
|
+
const doc = await this.loadDocumentInternal(docId);
|
|
238
|
+
console.log(`[BaseMindooDB] loadDocumentInternal returned for document ${docId}, result: ${doc ? 'success' : 'null'}`);
|
|
239
|
+
if (doc) {
|
|
240
|
+
console.log(`[BaseMindooDB] Successfully reloaded document ${docId}, updating index`);
|
|
241
|
+
this.updateIndex(docId, doc.lastModified, doc.isDeleted);
|
|
242
|
+
console.log(`[BaseMindooDB] Updated index for document ${docId} (lastModified: ${doc.lastModified}, isDeleted: ${doc.isDeleted})`);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
console.warn(`[BaseMindooDB] Document ${docId} returned null from loadDocumentInternal`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
// Handle missing symmetric key gracefully - skip documents we can't decrypt
|
|
250
|
+
if (error instanceof errors_1.SymmetricKeyNotFoundError) {
|
|
251
|
+
console.log(`[BaseMindooDB] Skipping document ${docId} - missing key: ${error.keyId}`);
|
|
252
|
+
// Mark entry IDs as processed so we don't retry them
|
|
253
|
+
this.processedEntryIds.push(...entryMetadataList.map(em => em.id));
|
|
254
|
+
continue; // Skip to next document
|
|
255
|
+
}
|
|
256
|
+
console.error(`[BaseMindooDB] ===== ERROR processing document ${docId} in syncStoreChanges =====`);
|
|
257
|
+
console.error(`[BaseMindooDB] Error type: ${error instanceof Error ? error.constructor.name : typeof error}`);
|
|
258
|
+
console.error(`[BaseMindooDB] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
259
|
+
if (error instanceof Error && error.stack) {
|
|
260
|
+
console.error(`[BaseMindooDB] Error stack: ${error.stack}`);
|
|
261
|
+
}
|
|
262
|
+
// Re-throw the error so we can see what's happening in the test
|
|
263
|
+
console.error(`[BaseMindooDB] Re-throwing error for document ${docId}`);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Append new entry IDs to our processed list
|
|
268
|
+
this.processedEntryIds.push(...newEntryMetadata.map(em => em.id));
|
|
269
|
+
console.log(`[BaseMindooDB] Synced ${newEntryMetadata.length} new entries, index now has ${this.index.length} documents`);
|
|
270
|
+
}
|
|
271
|
+
getStore() {
|
|
272
|
+
return this.store;
|
|
273
|
+
}
|
|
274
|
+
getAttachmentStore() {
|
|
275
|
+
return this.attachmentStore;
|
|
276
|
+
}
|
|
277
|
+
getTenant() {
|
|
278
|
+
return this.tenant;
|
|
279
|
+
}
|
|
280
|
+
async createDocument() {
|
|
281
|
+
return this.createEncryptedDocument("default");
|
|
282
|
+
}
|
|
283
|
+
async createEncryptedDocument(decryptionKeyId) {
|
|
284
|
+
return this.createDocumentInternal(decryptionKeyId || "default");
|
|
285
|
+
}
|
|
286
|
+
async createDocumentWithSigningKey(signingKeyPair, signingKeyPassword, decryptionKeyId) {
|
|
287
|
+
return this.createDocumentInternal(decryptionKeyId || "default", signingKeyPair, signingKeyPassword);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Internal method to create a new document.
|
|
291
|
+
* Handles both regular document creation (signed by current user) and
|
|
292
|
+
* document creation with a custom signing key (e.g., for directory operations).
|
|
293
|
+
*/
|
|
294
|
+
async createDocumentInternal(decryptionKeyId, signingKeyPair, signingKeyPassword) {
|
|
295
|
+
const keyId = decryptionKeyId;
|
|
296
|
+
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
297
|
+
// Admin-only validation: only admin key can modify data in admin-only databases
|
|
298
|
+
if (this._isAdminOnlyDb) {
|
|
299
|
+
const adminPublicKey = this.getAdminPublicKey();
|
|
300
|
+
const signerPublicKey = useCustomSigningKey
|
|
301
|
+
? signingKeyPair.publicKey
|
|
302
|
+
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
303
|
+
if (signerPublicKey !== adminPublicKey) {
|
|
304
|
+
throw new Error("Admin-only database: only the admin key can modify data");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Generate UUID7 for document ID
|
|
308
|
+
const docId = (0, uuid_1.v7)();
|
|
309
|
+
console.log(`[BaseMindooDB] Creating document ${docId} with key ${keyId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
|
|
310
|
+
// Create initial Automerge document
|
|
311
|
+
const initialDoc = Automerge.init();
|
|
312
|
+
// Create the first change
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
console.log(`[BaseMindooDB] Creating initial Automerge change for document ${docId}`);
|
|
315
|
+
let newDoc;
|
|
316
|
+
try {
|
|
317
|
+
newDoc = Automerge.change(initialDoc, (doc) => {
|
|
318
|
+
// Store metadata in the document payload
|
|
319
|
+
// We need to modify the document to ensure a change is created
|
|
320
|
+
doc._attachments = [];
|
|
321
|
+
});
|
|
322
|
+
console.log(`[BaseMindooDB] Successfully created Automerge change, document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
console.error(`[BaseMindooDB] Error in Automerge.change for document ${docId}:`, error);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
// Get the change bytes from the document
|
|
329
|
+
console.log(`[BaseMindooDB] Getting change bytes from document ${docId}`);
|
|
330
|
+
const changeBytes = Automerge.getLastLocalChange(newDoc);
|
|
331
|
+
if (!changeBytes) {
|
|
332
|
+
throw new Error("Failed to get change bytes from Automerge document");
|
|
333
|
+
}
|
|
334
|
+
console.log(`[BaseMindooDB] Got change bytes: ${changeBytes.length} bytes`);
|
|
335
|
+
// Decode the change to get hash and dependencies
|
|
336
|
+
console.log(`[BaseMindooDB] Decoding change to get hash and dependencies`);
|
|
337
|
+
let decodedChange;
|
|
338
|
+
try {
|
|
339
|
+
decodedChange = Automerge.decodeChange(changeBytes);
|
|
340
|
+
console.log(`[BaseMindooDB] Successfully decoded change, hash: ${decodedChange.hash}, deps: ${decodedChange.deps?.length || 0}`);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
console.error(`[BaseMindooDB] Error decoding change for document ${docId}:`, error);
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
const automergeHash = decodedChange.hash;
|
|
347
|
+
const automergeDepHashes = decodedChange.deps || []; // First change has no dependencies
|
|
348
|
+
// Encrypt the change payload first
|
|
349
|
+
console.log(`[BaseMindooDB] Encrypting change payload for document ${docId}`);
|
|
350
|
+
const encryptedPayload = await this.tenant.encryptPayload(changeBytes, keyId);
|
|
351
|
+
console.log(`[BaseMindooDB] Encrypted payload: ${changeBytes.length} -> ${encryptedPayload.length} bytes`);
|
|
352
|
+
// Compute content hash from encrypted data
|
|
353
|
+
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
|
|
354
|
+
console.log(`[BaseMindooDB] Computed content hash: ${contentHash.substring(0, 16)}...`);
|
|
355
|
+
// Generate entry ID with blockchain-like chaining
|
|
356
|
+
const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
|
|
357
|
+
console.log(`[BaseMindooDB] Generated entry ID: ${entryId}`);
|
|
358
|
+
// Resolve Automerge dependency hashes to entry IDs (empty for first change)
|
|
359
|
+
const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
|
|
360
|
+
// Sign the encrypted payload - either with custom key or current user's key
|
|
361
|
+
let signature;
|
|
362
|
+
let createdByPublicKey;
|
|
363
|
+
if (useCustomSigningKey) {
|
|
364
|
+
console.log(`[BaseMindooDB] Signing encrypted payload for document ${docId} with provided key`);
|
|
365
|
+
signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
|
|
366
|
+
createdByPublicKey = signingKeyPair.publicKey;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
console.log(`[BaseMindooDB] Signing encrypted payload for document ${docId}`);
|
|
370
|
+
const currentUser = await this.tenant.getCurrentUserId();
|
|
371
|
+
signature = await this.tenant.signPayload(encryptedPayload);
|
|
372
|
+
createdByPublicKey = currentUser.userSigningPublicKey;
|
|
373
|
+
}
|
|
374
|
+
console.log(`[BaseMindooDB] Signed payload, signature length: ${signature.length} bytes`);
|
|
375
|
+
// Create entry metadata
|
|
376
|
+
const entryMetadata = {
|
|
377
|
+
entryType: "doc_create",
|
|
378
|
+
id: entryId,
|
|
379
|
+
contentHash,
|
|
380
|
+
docId,
|
|
381
|
+
dependencyIds,
|
|
382
|
+
createdAt: now,
|
|
383
|
+
createdByPublicKey,
|
|
384
|
+
decryptionKeyId: keyId,
|
|
385
|
+
signature,
|
|
386
|
+
originalSize: changeBytes.length,
|
|
387
|
+
encryptedSize: encryptedPayload.length,
|
|
388
|
+
};
|
|
389
|
+
// Create full entry object
|
|
390
|
+
const fullEntry = {
|
|
391
|
+
...entryMetadata,
|
|
392
|
+
encryptedData: encryptedPayload,
|
|
393
|
+
};
|
|
394
|
+
// Store entry
|
|
395
|
+
await this.store.putEntries([fullEntry]);
|
|
396
|
+
// Register automerge hash -> entry ID mapping
|
|
397
|
+
this.registerAutomergeHashMapping(docId, automergeHash, entryId);
|
|
398
|
+
// Create internal document representation
|
|
399
|
+
const internalDoc = {
|
|
400
|
+
id: docId,
|
|
401
|
+
doc: newDoc,
|
|
402
|
+
createdAt: now,
|
|
403
|
+
lastModified: now,
|
|
404
|
+
decryptionKeyId: keyId,
|
|
405
|
+
isDeleted: false,
|
|
406
|
+
};
|
|
407
|
+
// Update cache and index
|
|
408
|
+
this.docCache.set(docId, internalDoc);
|
|
409
|
+
this.updateIndex(docId, internalDoc.lastModified, false);
|
|
410
|
+
console.log(`[BaseMindooDB] Document ${docId} created successfully`);
|
|
411
|
+
console.log(`[BaseMindooDB] Document ${docId} cached and indexed (lastModified: ${internalDoc.lastModified})`);
|
|
412
|
+
return this.wrapDocument(internalDoc);
|
|
413
|
+
}
|
|
414
|
+
async getDocument(docId) {
|
|
415
|
+
const internalDoc = await this.loadDocumentInternal(docId);
|
|
416
|
+
if (!internalDoc) {
|
|
417
|
+
throw new Error(`Document ${docId} not found`);
|
|
418
|
+
}
|
|
419
|
+
if (internalDoc.isDeleted) {
|
|
420
|
+
throw new Error(`Document ${docId} has been deleted`);
|
|
421
|
+
}
|
|
422
|
+
return this.wrapDocument(internalDoc);
|
|
423
|
+
}
|
|
424
|
+
async getDocumentAtTimestamp(docId, timestamp) {
|
|
425
|
+
console.log(`[BaseMindooDB] Getting document ${docId} at timestamp ${timestamp}`);
|
|
426
|
+
// Get all entry metadata for this document
|
|
427
|
+
const allEntryMetadata = await this.store.findNewEntriesForDoc([], docId);
|
|
428
|
+
// Filter entries up to the timestamp
|
|
429
|
+
const relevantEntries = allEntryMetadata
|
|
430
|
+
.filter(em => em.createdAt <= timestamp)
|
|
431
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
432
|
+
if (relevantEntries.length === 0) {
|
|
433
|
+
return null; // Document didn't exist at that time
|
|
434
|
+
}
|
|
435
|
+
// Load entries
|
|
436
|
+
const entries = await this.store.getEntries(relevantEntries.map(em => em.id));
|
|
437
|
+
// Apply changes in order
|
|
438
|
+
let doc = Automerge.init();
|
|
439
|
+
for (const entryData of entries) {
|
|
440
|
+
// Admin-only mode: only accept entries signed by the admin key
|
|
441
|
+
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
442
|
+
console.warn(`[BaseMindooDB] Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
// Verify signature against the encrypted payload (no decryption needed)
|
|
446
|
+
// We sign the encrypted payload, so anyone can verify signatures without decryption keys
|
|
447
|
+
const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
|
|
448
|
+
if (!isValid) {
|
|
449
|
+
console.warn(`[BaseMindooDB] Invalid signature for entry ${entryData.id}, skipping`);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
// Decrypt payload (only after signature verification passes)
|
|
453
|
+
const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
|
|
454
|
+
// Apply change using loadIncremental - this is the recommended way for binary change data
|
|
455
|
+
doc = Automerge.loadIncremental(doc, decryptedPayload);
|
|
456
|
+
// Register the automerge hash -> entry ID mapping for future dependency resolution
|
|
457
|
+
const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
|
|
458
|
+
if (parsed) {
|
|
459
|
+
this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Check if document was deleted at this timestamp
|
|
463
|
+
// Look for a "doc_delete" type entry in the relevant entries
|
|
464
|
+
const hasDeleteEntry = relevantEntries.some(em => em.entryType === "doc_delete" && em.createdAt <= timestamp);
|
|
465
|
+
if (hasDeleteEntry) {
|
|
466
|
+
return null; // Document was deleted at this time
|
|
467
|
+
}
|
|
468
|
+
// Find the first entry to get createdAt and decryptionKeyId
|
|
469
|
+
const firstEntry = relevantEntries.length > 0 ? relevantEntries[0] : null;
|
|
470
|
+
const createdAt = firstEntry ? firstEntry.createdAt : timestamp;
|
|
471
|
+
const decryptionKeyId = firstEntry ? firstEntry.decryptionKeyId : "default";
|
|
472
|
+
const internalDoc = {
|
|
473
|
+
id: docId,
|
|
474
|
+
doc,
|
|
475
|
+
createdAt,
|
|
476
|
+
lastModified: timestamp,
|
|
477
|
+
decryptionKeyId,
|
|
478
|
+
isDeleted: false,
|
|
479
|
+
};
|
|
480
|
+
return this.wrapDocument(internalDoc);
|
|
481
|
+
}
|
|
482
|
+
async getAllDocumentIds() {
|
|
483
|
+
// Return all non-deleted document IDs from index
|
|
484
|
+
const docIds = [];
|
|
485
|
+
for (const entry of this.index) {
|
|
486
|
+
if (!entry.isDeleted) {
|
|
487
|
+
docIds.push(entry.docId);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return docIds;
|
|
491
|
+
}
|
|
492
|
+
async deleteDocument(docId) {
|
|
493
|
+
return this.deleteDocInternal(docId);
|
|
494
|
+
}
|
|
495
|
+
async deleteDocumentWithSigningKey(docId, signingKeyPair, signingKeyPassword) {
|
|
496
|
+
return this.deleteDocInternal(docId, signingKeyPair, signingKeyPassword);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Internal method to delete a document.
|
|
500
|
+
* Handles both regular deletion (signed by current user) and
|
|
501
|
+
* deletion with a custom signing key (e.g., for directory operations).
|
|
502
|
+
*/
|
|
503
|
+
async deleteDocInternal(docId, signingKeyPair, signingKeyPassword) {
|
|
504
|
+
const useCustomSigningKey = signingKeyPair !== undefined && signingKeyPassword !== undefined;
|
|
505
|
+
console.log(`[BaseMindooDB] Deleting document ${docId}${useCustomSigningKey ? ' using custom signing key' : ''}`);
|
|
506
|
+
// Admin-only validation: only admin key can modify data in admin-only databases
|
|
507
|
+
if (this._isAdminOnlyDb) {
|
|
508
|
+
const adminPublicKey = this.getAdminPublicKey();
|
|
509
|
+
const signerPublicKey = useCustomSigningKey
|
|
510
|
+
? signingKeyPair.publicKey
|
|
511
|
+
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
512
|
+
if (signerPublicKey !== adminPublicKey) {
|
|
513
|
+
throw new Error("Admin-only database: only the admin key can modify data");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Get current document
|
|
517
|
+
const internalDoc = await this.loadDocumentInternal(docId);
|
|
518
|
+
if (!internalDoc || internalDoc.isDeleted) {
|
|
519
|
+
throw new Error(`Document ${docId} not found or already deleted`);
|
|
520
|
+
}
|
|
521
|
+
// Create deletion change by clearing all fields from the document
|
|
522
|
+
// This ensures Automerge produces actual change bytes
|
|
523
|
+
// The deletion is also tracked via the "doc_delete" type entry in the append-only store
|
|
524
|
+
const newDoc = Automerge.change(internalDoc.doc, (doc) => {
|
|
525
|
+
// Remove all fields from the document
|
|
526
|
+
for (const key of Object.keys(doc)) {
|
|
527
|
+
delete doc[key];
|
|
528
|
+
}
|
|
529
|
+
// Mark as deleted for clarity
|
|
530
|
+
doc._deleted = true;
|
|
531
|
+
});
|
|
532
|
+
// Get the change bytes from the document
|
|
533
|
+
const changeBytes = Automerge.getLastLocalChange(newDoc);
|
|
534
|
+
if (!changeBytes) {
|
|
535
|
+
throw new Error("Failed to get change bytes from Automerge document");
|
|
536
|
+
}
|
|
537
|
+
// Decode the change to get hash and dependencies
|
|
538
|
+
const decodedChange = Automerge.decodeChange(changeBytes);
|
|
539
|
+
const automergeHash = decodedChange.hash;
|
|
540
|
+
const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
|
|
541
|
+
// Encrypt the change payload first
|
|
542
|
+
const encryptedPayload = await this.tenant.encryptPayload(changeBytes, internalDoc.decryptionKeyId);
|
|
543
|
+
// Compute content hash from encrypted data
|
|
544
|
+
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
|
|
545
|
+
// Generate entry ID with blockchain-like chaining
|
|
546
|
+
const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
|
|
547
|
+
// Resolve Automerge dependency hashes to entry IDs
|
|
548
|
+
const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
|
|
549
|
+
// Sign the encrypted payload - either with custom key or current user's key
|
|
550
|
+
let signature;
|
|
551
|
+
let createdByPublicKey;
|
|
552
|
+
if (useCustomSigningKey) {
|
|
553
|
+
console.log(`[BaseMindooDB] Signing deletion for document ${docId} with provided key`);
|
|
554
|
+
signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
|
|
555
|
+
createdByPublicKey = signingKeyPair.publicKey;
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
console.log(`[BaseMindooDB] Signing deletion for document ${docId} with current user's key`);
|
|
559
|
+
const currentUser = await this.tenant.getCurrentUserId();
|
|
560
|
+
signature = await this.tenant.signPayload(encryptedPayload);
|
|
561
|
+
createdByPublicKey = currentUser.userSigningPublicKey;
|
|
562
|
+
}
|
|
563
|
+
// Create entry metadata with type "doc_delete" to mark this as a deletion entry in the store
|
|
564
|
+
const entryMetadata = {
|
|
565
|
+
entryType: "doc_delete",
|
|
566
|
+
id: entryId,
|
|
567
|
+
contentHash,
|
|
568
|
+
docId,
|
|
569
|
+
dependencyIds,
|
|
570
|
+
createdAt: Date.now(),
|
|
571
|
+
createdByPublicKey,
|
|
572
|
+
decryptionKeyId: internalDoc.decryptionKeyId,
|
|
573
|
+
signature,
|
|
574
|
+
originalSize: changeBytes.length,
|
|
575
|
+
encryptedSize: encryptedPayload.length,
|
|
576
|
+
};
|
|
577
|
+
// Create full entry object
|
|
578
|
+
const fullEntry = {
|
|
579
|
+
...entryMetadata,
|
|
580
|
+
encryptedData: encryptedPayload,
|
|
581
|
+
};
|
|
582
|
+
// Store entry
|
|
583
|
+
await this.store.putEntries([fullEntry]);
|
|
584
|
+
// Register automerge hash -> entry ID mapping
|
|
585
|
+
this.registerAutomergeHashMapping(docId, automergeHash, entryId);
|
|
586
|
+
// Update cache and index
|
|
587
|
+
internalDoc.isDeleted = true;
|
|
588
|
+
internalDoc.lastModified = Date.now();
|
|
589
|
+
this.docCache.set(docId, internalDoc);
|
|
590
|
+
this.updateIndex(docId, internalDoc.lastModified, true);
|
|
591
|
+
console.log(`[BaseMindooDB] Document ${docId} deleted successfully`);
|
|
592
|
+
}
|
|
593
|
+
async changeDoc(doc, changeFunc) {
|
|
594
|
+
return this.changeDocInternal(doc, changeFunc);
|
|
595
|
+
}
|
|
596
|
+
async changeDocWithSigningKey(doc, changeFunc, signingKeyPair, signingKeyPassword) {
|
|
597
|
+
return this.changeDocInternal(doc, changeFunc, signingKeyPair, signingKeyPassword);
|
|
598
|
+
}
|
|
599
|
+
async changeDocInternal(doc, changeFunc, signingKeyPair, signingKeyPassword) {
|
|
600
|
+
const docId = doc.getId();
|
|
601
|
+
const useCustomKey = signingKeyPair && signingKeyPassword;
|
|
602
|
+
console.log(`[BaseMindooDB] ===== ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'} called for document ${docId} =====`);
|
|
603
|
+
// Admin-only validation: only admin key can modify data in admin-only databases
|
|
604
|
+
if (this._isAdminOnlyDb) {
|
|
605
|
+
const adminPublicKey = this.getAdminPublicKey();
|
|
606
|
+
const signerPublicKey = useCustomKey
|
|
607
|
+
? signingKeyPair.publicKey
|
|
608
|
+
: (await this.tenant.getCurrentUserId()).userSigningPublicKey;
|
|
609
|
+
if (signerPublicKey !== adminPublicKey) {
|
|
610
|
+
throw new Error("Admin-only database: only the admin key can modify data");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Get internal document from cache or load it
|
|
614
|
+
let internalDoc = this.docCache.get(docId);
|
|
615
|
+
if (!internalDoc) {
|
|
616
|
+
console.log(`[BaseMindooDB] Document ${docId} not in cache, loading from store`);
|
|
617
|
+
const loadedDoc = await this.loadDocumentInternal(docId);
|
|
618
|
+
if (!loadedDoc) {
|
|
619
|
+
throw new Error(`Document ${docId} not found`);
|
|
620
|
+
}
|
|
621
|
+
internalDoc = loadedDoc;
|
|
622
|
+
console.log(`[BaseMindooDB] Successfully loaded document ${docId} from store for ${useCustomKey ? 'changeDocWithSigningKey' : 'changeDoc'}`);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
console.log(`[BaseMindooDB] Document ${docId} found in cache`);
|
|
626
|
+
}
|
|
627
|
+
if (internalDoc.isDeleted) {
|
|
628
|
+
throw new Error(`Document ${docId} has been deleted`);
|
|
629
|
+
}
|
|
630
|
+
// Apply the change function
|
|
631
|
+
const now = Date.now();
|
|
632
|
+
console.log(`[BaseMindooDB] Applying change function to document ${docId}`);
|
|
633
|
+
console.log(`[BaseMindooDB] Document state before change: heads=${JSON.stringify(Automerge.getHeads(internalDoc.doc))}`);
|
|
634
|
+
// For async callbacks, we need to handle document modifications carefully.
|
|
635
|
+
// Automerge.change() requires synchronous modifications, so we'll:
|
|
636
|
+
// 1. Execute the async callback to do any async work (like signing)
|
|
637
|
+
// 2. Apply document modifications synchronously within Automerge.change()
|
|
638
|
+
//
|
|
639
|
+
// We use a two-phase approach: the callback can do async work and collect
|
|
640
|
+
// what needs to be changed, then we apply those changes in Automerge.change()
|
|
641
|
+
const pendingChanges = new Map();
|
|
642
|
+
const pendingDeletions = new Set();
|
|
643
|
+
// Track pending attachment operations
|
|
644
|
+
const pendingAttachmentAdditions = [];
|
|
645
|
+
const pendingAttachmentRemovals = new Set();
|
|
646
|
+
// Map of attachmentId -> {lastChunkId, sizeIncrease} for appends
|
|
647
|
+
const pendingAttachmentAppends = new Map();
|
|
648
|
+
// Reference to db for closures
|
|
649
|
+
const db = this;
|
|
650
|
+
// Guard flag to prevent operations after callback completes
|
|
651
|
+
// This ensures changes can only be made during the callback execution
|
|
652
|
+
let isCallbackActive = true;
|
|
653
|
+
const throwIfCallbackInactive = (methodName) => {
|
|
654
|
+
if (!isCallbackActive) {
|
|
655
|
+
throw new Error(`${methodName}() cannot be called after changeDoc() callback has completed. Document changes can only be made within the callback.`);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
// Create a document wrapper that collects changes
|
|
659
|
+
const collectingDoc = {
|
|
660
|
+
getDatabase: () => this,
|
|
661
|
+
getId: () => docId,
|
|
662
|
+
getCreatedAt: () => internalDoc.createdAt,
|
|
663
|
+
getLastModified: () => internalDoc.lastModified,
|
|
664
|
+
isDeleted: () => false,
|
|
665
|
+
getData: () => {
|
|
666
|
+
// Return a proxy that collects property assignments and deletions
|
|
667
|
+
const currentData = internalDoc.doc;
|
|
668
|
+
return new Proxy(currentData, {
|
|
669
|
+
set: (target, prop, value) => {
|
|
670
|
+
throwIfCallbackInactive('set property');
|
|
671
|
+
if (typeof prop === 'string') {
|
|
672
|
+
// If this property was marked for deletion, remove it from deletions
|
|
673
|
+
pendingDeletions.delete(prop);
|
|
674
|
+
// Track the change
|
|
675
|
+
pendingChanges.set(prop, value);
|
|
676
|
+
// Also set on the target for immediate access
|
|
677
|
+
target[prop] = value;
|
|
678
|
+
}
|
|
679
|
+
return true;
|
|
680
|
+
},
|
|
681
|
+
deleteProperty: (target, prop) => {
|
|
682
|
+
throwIfCallbackInactive('delete property');
|
|
683
|
+
if (typeof prop === 'string') {
|
|
684
|
+
// Mark for deletion
|
|
685
|
+
pendingDeletions.add(prop);
|
|
686
|
+
// Remove from pending changes if it was there
|
|
687
|
+
pendingChanges.delete(prop);
|
|
688
|
+
// Also delete from the target for immediate access
|
|
689
|
+
delete target[prop];
|
|
690
|
+
}
|
|
691
|
+
return true;
|
|
692
|
+
},
|
|
693
|
+
get: (target, prop) => {
|
|
694
|
+
// If marked for deletion, return undefined
|
|
695
|
+
if (typeof prop === 'string' && pendingDeletions.has(prop)) {
|
|
696
|
+
return undefined;
|
|
697
|
+
}
|
|
698
|
+
// Check pending changes first, then target
|
|
699
|
+
if (typeof prop === 'string' && pendingChanges.has(prop)) {
|
|
700
|
+
return pendingChanges.get(prop);
|
|
701
|
+
}
|
|
702
|
+
return target[prop];
|
|
703
|
+
},
|
|
704
|
+
has: (target, prop) => {
|
|
705
|
+
// If marked for deletion, it doesn't exist
|
|
706
|
+
if (typeof prop === 'string' && pendingDeletions.has(prop)) {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
// Check pending changes first, then target
|
|
710
|
+
if (typeof prop === 'string' && pendingChanges.has(prop)) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
return prop in target;
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
},
|
|
717
|
+
// ========== Attachment Write Methods (work in changeDoc context) ==========
|
|
718
|
+
addAttachment: async (fileData, fileName, mimeType, keyId) => {
|
|
719
|
+
throwIfCallbackInactive('addAttachment');
|
|
720
|
+
const decryptionKeyId = keyId || internalDoc.decryptionKeyId;
|
|
721
|
+
const ref = await db.addAttachmentInternal(docId, fileData, fileName, mimeType, decryptionKeyId, now);
|
|
722
|
+
pendingAttachmentAdditions.push(ref);
|
|
723
|
+
return ref;
|
|
724
|
+
},
|
|
725
|
+
addAttachmentStream: async (dataStream, fileName, mimeType, keyId) => {
|
|
726
|
+
throwIfCallbackInactive('addAttachmentStream');
|
|
727
|
+
const decryptionKeyId = keyId || internalDoc.decryptionKeyId;
|
|
728
|
+
const ref = await db.addAttachmentStreamInternal(docId, dataStream, fileName, mimeType, decryptionKeyId, now);
|
|
729
|
+
pendingAttachmentAdditions.push(ref);
|
|
730
|
+
return ref;
|
|
731
|
+
},
|
|
732
|
+
removeAttachment: async (attachmentId) => {
|
|
733
|
+
throwIfCallbackInactive('removeAttachment');
|
|
734
|
+
// Check if attachment exists (either in current doc or pending additions)
|
|
735
|
+
const payload = internalDoc.doc;
|
|
736
|
+
const existingAttachments = payload._attachments || [];
|
|
737
|
+
const existsInDoc = existingAttachments.some(a => a.attachmentId === attachmentId);
|
|
738
|
+
const existsInPending = pendingAttachmentAdditions.some(a => a.attachmentId === attachmentId);
|
|
739
|
+
if (!existsInDoc && !existsInPending) {
|
|
740
|
+
throw new Error(`Attachment ${attachmentId} not found in document ${docId}`);
|
|
741
|
+
}
|
|
742
|
+
// If it was added in this same changeDoc call, just remove from pending
|
|
743
|
+
const pendingIndex = pendingAttachmentAdditions.findIndex(a => a.attachmentId === attachmentId);
|
|
744
|
+
if (pendingIndex >= 0) {
|
|
745
|
+
pendingAttachmentAdditions.splice(pendingIndex, 1);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
// Mark for removal from existing attachments
|
|
749
|
+
pendingAttachmentRemovals.add(attachmentId);
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
appendToAttachment: async (attachmentId, data) => {
|
|
753
|
+
throwIfCallbackInactive('appendToAttachment');
|
|
754
|
+
// Find the attachment reference
|
|
755
|
+
const payload = internalDoc.doc;
|
|
756
|
+
const existingAttachments = payload._attachments || [];
|
|
757
|
+
let ref = existingAttachments.find(a => a.attachmentId === attachmentId);
|
|
758
|
+
// Also check pending additions
|
|
759
|
+
if (!ref) {
|
|
760
|
+
ref = pendingAttachmentAdditions.find(a => a.attachmentId === attachmentId);
|
|
761
|
+
}
|
|
762
|
+
if (!ref) {
|
|
763
|
+
throw new Error(`Attachment ${attachmentId} not found in document ${docId}`);
|
|
764
|
+
}
|
|
765
|
+
// Determine the previous lastChunkId (might have been updated by previous append in this changeDoc)
|
|
766
|
+
let prevLastChunkId = ref.lastChunkId;
|
|
767
|
+
const prevAppend = pendingAttachmentAppends.get(attachmentId);
|
|
768
|
+
if (prevAppend) {
|
|
769
|
+
prevLastChunkId = prevAppend.lastChunkId;
|
|
770
|
+
}
|
|
771
|
+
// Append the data by creating new chunks
|
|
772
|
+
const { lastChunkId, sizeIncrease } = await db.appendToAttachmentInternal(docId, attachmentId, ref.decryptionKeyId, prevLastChunkId, data, now);
|
|
773
|
+
// Track the append
|
|
774
|
+
const existingAppend = pendingAttachmentAppends.get(attachmentId);
|
|
775
|
+
if (existingAppend) {
|
|
776
|
+
existingAppend.lastChunkId = lastChunkId;
|
|
777
|
+
existingAppend.sizeIncrease += sizeIncrease;
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
pendingAttachmentAppends.set(attachmentId, { lastChunkId, sizeIncrease });
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
// ========== Attachment Read Methods (also work in changeDoc context) ==========
|
|
784
|
+
getAttachments: () => {
|
|
785
|
+
const payload = internalDoc.doc;
|
|
786
|
+
const existing = payload._attachments || [];
|
|
787
|
+
// Filter out removals and add pending additions
|
|
788
|
+
const filtered = existing.filter(a => !pendingAttachmentRemovals.has(a.attachmentId));
|
|
789
|
+
return [...filtered, ...pendingAttachmentAdditions];
|
|
790
|
+
},
|
|
791
|
+
getAttachment: async (attachmentId) => {
|
|
792
|
+
return db.getAttachmentInternal(docId, attachmentId);
|
|
793
|
+
},
|
|
794
|
+
getAttachmentRange: async (attachmentId, startByte, endByte) => {
|
|
795
|
+
return db.getAttachmentRangeInternal(docId, attachmentId, startByte, endByte);
|
|
796
|
+
},
|
|
797
|
+
streamAttachment: (attachmentId, startOffset = 0) => {
|
|
798
|
+
return db.streamAttachmentInternal(docId, attachmentId, startOffset);
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
// Execute the async callback (this may do async operations like signing)
|
|
802
|
+
await changeFunc(collectingDoc);
|
|
803
|
+
// Deactivate the callback guard - no more changes can be made via collectingDoc
|
|
804
|
+
isCallbackActive = false;
|
|
805
|
+
// Now apply the collected changes synchronously in Automerge.change()
|
|
806
|
+
let newDoc;
|
|
807
|
+
try {
|
|
808
|
+
newDoc = Automerge.change(internalDoc.doc, (automergeDoc) => {
|
|
809
|
+
// Apply all pending changes (sets/updates)
|
|
810
|
+
for (const [key, value] of pendingChanges) {
|
|
811
|
+
automergeDoc[key] = value;
|
|
812
|
+
}
|
|
813
|
+
// Apply all pending deletions
|
|
814
|
+
for (const key of pendingDeletions) {
|
|
815
|
+
delete automergeDoc[key];
|
|
816
|
+
}
|
|
817
|
+
// Apply pending attachment changes
|
|
818
|
+
if (pendingAttachmentAdditions.length > 0 || pendingAttachmentRemovals.size > 0 || pendingAttachmentAppends.size > 0) {
|
|
819
|
+
// Initialize _attachments array if needed
|
|
820
|
+
if (!automergeDoc._attachments) {
|
|
821
|
+
automergeDoc._attachments = [];
|
|
822
|
+
}
|
|
823
|
+
const attachments = automergeDoc._attachments;
|
|
824
|
+
// Remove attachments marked for removal
|
|
825
|
+
for (const attachmentId of pendingAttachmentRemovals) {
|
|
826
|
+
const index = attachments.findIndex(a => a.attachmentId === attachmentId);
|
|
827
|
+
if (index >= 0) {
|
|
828
|
+
attachments.splice(index, 1);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Apply appends (update lastChunkId and size)
|
|
832
|
+
for (const [attachmentId, { lastChunkId, sizeIncrease }] of pendingAttachmentAppends) {
|
|
833
|
+
const attachment = attachments.find(a => a.attachmentId === attachmentId);
|
|
834
|
+
if (attachment) {
|
|
835
|
+
attachment.lastChunkId = lastChunkId;
|
|
836
|
+
attachment.size += sizeIncrease;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// Add new attachments
|
|
840
|
+
for (const ref of pendingAttachmentAdditions) {
|
|
841
|
+
attachments.push(ref);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Update lastModified timestamp
|
|
845
|
+
automergeDoc._lastModified = now;
|
|
846
|
+
});
|
|
847
|
+
console.log(`[BaseMindooDB] Successfully applied change function, new document heads: ${JSON.stringify(Automerge.getHeads(newDoc))}`);
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
console.error(`[BaseMindooDB] Error in Automerge.change for document ${docId}:`, error);
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
853
|
+
// Get the change bytes from the document
|
|
854
|
+
console.log(`[BaseMindooDB] Getting change bytes from document ${docId}`);
|
|
855
|
+
const changeBytes = Automerge.getLastLocalChange(newDoc);
|
|
856
|
+
if (!changeBytes) {
|
|
857
|
+
//TODO decide if we just exit here or throw an error
|
|
858
|
+
throw new Error("Failed to get change bytes from Automerge document");
|
|
859
|
+
}
|
|
860
|
+
console.log(`[BaseMindooDB] Got change bytes: ${changeBytes.length} bytes`);
|
|
861
|
+
// Decode the change to get hash and dependencies
|
|
862
|
+
const decodedChange = Automerge.decodeChange(changeBytes);
|
|
863
|
+
const automergeHash = decodedChange.hash;
|
|
864
|
+
const automergeDepHashes = decodedChange.deps || []; // Dependencies from the decoded change
|
|
865
|
+
// Encrypt the change payload first
|
|
866
|
+
const encryptedPayload = await this.tenant.encryptPayload(changeBytes, internalDoc.decryptionKeyId);
|
|
867
|
+
// Compute content hash from encrypted data
|
|
868
|
+
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedPayload, this.getSubtle());
|
|
869
|
+
// Generate entry ID with blockchain-like chaining
|
|
870
|
+
const entryId = await (0, idGeneration_1.generateDocEntryId)(docId, automergeHash, automergeDepHashes, this.getSubtle());
|
|
871
|
+
console.log(`[BaseMindooDB] Generated entry ID for change: ${entryId}`);
|
|
872
|
+
// Resolve Automerge dependency hashes to entry IDs
|
|
873
|
+
const dependencyIds = this.resolveAutomergeDepsToEntryIds(docId, automergeDepHashes);
|
|
874
|
+
// Sign the encrypted payload - use custom key if provided, otherwise current user's key
|
|
875
|
+
let signature;
|
|
876
|
+
let createdByPublicKey;
|
|
877
|
+
if (useCustomKey) {
|
|
878
|
+
signature = await this.tenant.signPayloadWithKey(encryptedPayload, signingKeyPair, signingKeyPassword);
|
|
879
|
+
createdByPublicKey = signingKeyPair.publicKey;
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
const currentUser = await this.tenant.getCurrentUserId();
|
|
883
|
+
signature = await this.tenant.signPayload(encryptedPayload);
|
|
884
|
+
createdByPublicKey = currentUser.userSigningPublicKey;
|
|
885
|
+
}
|
|
886
|
+
// Create entry metadata
|
|
887
|
+
const entryMetadata = {
|
|
888
|
+
entryType: "doc_change",
|
|
889
|
+
id: entryId,
|
|
890
|
+
contentHash,
|
|
891
|
+
docId,
|
|
892
|
+
dependencyIds,
|
|
893
|
+
createdAt: Date.now(),
|
|
894
|
+
createdByPublicKey,
|
|
895
|
+
decryptionKeyId: internalDoc.decryptionKeyId,
|
|
896
|
+
signature,
|
|
897
|
+
originalSize: changeBytes.length,
|
|
898
|
+
encryptedSize: encryptedPayload.length,
|
|
899
|
+
};
|
|
900
|
+
// Create full entry object
|
|
901
|
+
const fullEntry = {
|
|
902
|
+
...entryMetadata,
|
|
903
|
+
encryptedData: encryptedPayload,
|
|
904
|
+
};
|
|
905
|
+
// Store entry
|
|
906
|
+
await this.store.putEntries([fullEntry]);
|
|
907
|
+
// Register automerge hash -> entry ID mapping
|
|
908
|
+
this.registerAutomergeHashMapping(docId, automergeHash, entryId);
|
|
909
|
+
// Update cache and index
|
|
910
|
+
internalDoc.doc = newDoc;
|
|
911
|
+
internalDoc.lastModified = Date.now();
|
|
912
|
+
this.docCache.set(docId, internalDoc);
|
|
913
|
+
this.updateIndex(docId, internalDoc.lastModified, internalDoc.isDeleted);
|
|
914
|
+
console.log(`[BaseMindooDB] Document ${docId} ${useCustomKey ? 'changed with custom signing key' : 'changed'} successfully`);
|
|
915
|
+
}
|
|
916
|
+
async processChangesSince(cursor, limit, callback) {
|
|
917
|
+
// Default to initial cursor if null is provided
|
|
918
|
+
const actualCursor = cursor ?? { lastModified: 0, docId: "" };
|
|
919
|
+
console.log(`[BaseMindooDB] Processing changes since cursor ${JSON.stringify(actualCursor)} (limit: ${limit})`);
|
|
920
|
+
// Find starting position using binary search
|
|
921
|
+
// We want to find the first entry that is greater than the cursor
|
|
922
|
+
let startIndex = 0;
|
|
923
|
+
if (this.index.length > 0) {
|
|
924
|
+
let left = 0;
|
|
925
|
+
let right = this.index.length - 1;
|
|
926
|
+
while (left <= right) {
|
|
927
|
+
const mid = Math.floor((left + right) / 2);
|
|
928
|
+
const entry = this.index[mid];
|
|
929
|
+
const cmp = this.compareIndexEntries({ docId: actualCursor.docId, lastModified: actualCursor.lastModified }, entry);
|
|
930
|
+
if (cmp < 0) {
|
|
931
|
+
right = mid - 1;
|
|
932
|
+
startIndex = mid;
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
left = mid + 1;
|
|
936
|
+
startIndex = mid + 1;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
// If we found an exact match, start from the next entry
|
|
940
|
+
if (startIndex < this.index.length) {
|
|
941
|
+
const entry = this.index[startIndex];
|
|
942
|
+
if (entry.lastModified === actualCursor.lastModified && entry.docId === actualCursor.docId) {
|
|
943
|
+
startIndex++;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
// Process documents in order from the starting position
|
|
948
|
+
let processedCount = 0;
|
|
949
|
+
let lastCursor = actualCursor;
|
|
950
|
+
for (let i = startIndex; i < this.index.length && processedCount < limit; i++) {
|
|
951
|
+
const entry = this.index[i];
|
|
952
|
+
// Skip deleted documents (they're still in index for tracking, but shouldn't be processed)
|
|
953
|
+
if (entry.isDeleted) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
console.log(`[BaseMindooDB] Processing document ${entry.docId} from index (lastModified: ${entry.lastModified})`);
|
|
958
|
+
const doc = await this.getDocument(entry.docId);
|
|
959
|
+
console.log(`[BaseMindooDB] Successfully loaded document ${entry.docId}`);
|
|
960
|
+
// Create cursor for current document
|
|
961
|
+
const currentCursor = {
|
|
962
|
+
lastModified: entry.lastModified,
|
|
963
|
+
docId: entry.docId,
|
|
964
|
+
};
|
|
965
|
+
const shouldContinue = callback(doc, currentCursor);
|
|
966
|
+
// Update cursor to the last successfully processed document
|
|
967
|
+
lastCursor = currentCursor;
|
|
968
|
+
processedCount++;
|
|
969
|
+
// Stop if callback returns false
|
|
970
|
+
if (shouldContinue === false) {
|
|
971
|
+
console.log(`[BaseMindooDB] Callback requested to stop processing`);
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
catch (error) {
|
|
976
|
+
console.error(`[BaseMindooDB] Error processing document ${entry.docId}:`, error);
|
|
977
|
+
// Stop processing on error
|
|
978
|
+
throw error;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
console.log(`[BaseMindooDB] Processed ${processedCount} changes, last cursor: ${JSON.stringify(lastCursor)}`);
|
|
982
|
+
return lastCursor;
|
|
983
|
+
}
|
|
984
|
+
async *iterateChangesSince(cursor, pageSize = 100) {
|
|
985
|
+
console.log(`[BaseMindooDB] Starting iteration from cursor ${JSON.stringify(cursor)} (pageSize: ${pageSize})`);
|
|
986
|
+
let currentCursor = cursor;
|
|
987
|
+
while (true) {
|
|
988
|
+
// Collect documents from this page
|
|
989
|
+
const pageResults = [];
|
|
990
|
+
let documentsProcessed = 0;
|
|
991
|
+
// Process one page of changes
|
|
992
|
+
const returnedCursor = await this.processChangesSince(currentCursor, pageSize, (doc, docCursor) => {
|
|
993
|
+
documentsProcessed++;
|
|
994
|
+
pageResults.push({ doc, cursor: docCursor });
|
|
995
|
+
// Always continue processing the page
|
|
996
|
+
return true;
|
|
997
|
+
});
|
|
998
|
+
// Yield all documents from this page
|
|
999
|
+
for (const result of pageResults) {
|
|
1000
|
+
yield result;
|
|
1001
|
+
}
|
|
1002
|
+
// If we processed fewer documents than the page size, we've reached the end
|
|
1003
|
+
if (documentsProcessed < pageSize) {
|
|
1004
|
+
console.log(`[BaseMindooDB] Reached end of documents, processed ${documentsProcessed} in last page`);
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
// If the cursor didn't advance, we've reached the end
|
|
1008
|
+
if (currentCursor !== null &&
|
|
1009
|
+
returnedCursor.lastModified === currentCursor.lastModified &&
|
|
1010
|
+
returnedCursor.docId === currentCursor.docId) {
|
|
1011
|
+
console.log(`[BaseMindooDB] Cursor did not advance, reached end of documents`);
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
// Continue with the next page using the returned cursor
|
|
1015
|
+
currentCursor = returnedCursor;
|
|
1016
|
+
}
|
|
1017
|
+
console.log(`[BaseMindooDB] Iteration completed`);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Internal method to load a document from the content-addressed store
|
|
1021
|
+
*/
|
|
1022
|
+
async loadDocumentInternal(docId) {
|
|
1023
|
+
// Check cache first
|
|
1024
|
+
if (this.docCache.has(docId)) {
|
|
1025
|
+
console.log(`[BaseMindooDB] Document ${docId} found in cache, returning cached version`);
|
|
1026
|
+
return this.docCache.get(docId);
|
|
1027
|
+
}
|
|
1028
|
+
console.log(`[BaseMindooDB] ===== Starting to load document ${docId} from store =====`);
|
|
1029
|
+
// Get all entry metadata for this document
|
|
1030
|
+
// TODO: Implement loading from last snapshot if available
|
|
1031
|
+
console.log(`[BaseMindooDB] Getting all entry hashes for document ${docId}`);
|
|
1032
|
+
const allEntryMetadata = await this.store.findNewEntriesForDoc([], docId);
|
|
1033
|
+
console.log(`[BaseMindooDB] Found ${allEntryMetadata.length} total entry hashes for document ${docId}`);
|
|
1034
|
+
if (allEntryMetadata.length === 0) {
|
|
1035
|
+
console.log(`[BaseMindooDB] No entry hashes found for document ${docId}, returning null`);
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
// Log all entry types
|
|
1039
|
+
const entryTypes = allEntryMetadata.map(em => `${em.entryType}@${em.createdAt}`).join(', ');
|
|
1040
|
+
console.log(`[BaseMindooDB] Entry types for ${docId}: ${entryTypes}`);
|
|
1041
|
+
// Find the most recent snapshot (if any)
|
|
1042
|
+
const snapshots = allEntryMetadata.filter(em => em.entryType === "doc_snapshot");
|
|
1043
|
+
console.log(`[BaseMindooDB] Found ${snapshots.length} snapshot(s) for document ${docId}`);
|
|
1044
|
+
let startFromSnapshot = false;
|
|
1045
|
+
let snapshotMeta = null;
|
|
1046
|
+
if (snapshots.length > 0) {
|
|
1047
|
+
// Use the most recent snapshot
|
|
1048
|
+
snapshots.sort((a, b) => b.createdAt - a.createdAt);
|
|
1049
|
+
snapshotMeta = snapshots[0];
|
|
1050
|
+
startFromSnapshot = true;
|
|
1051
|
+
console.log(`[BaseMindooDB] Will start from snapshot ${snapshotMeta.id} created at ${snapshotMeta.createdAt}`);
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
console.log(`[BaseMindooDB] No snapshot found, will start from scratch`);
|
|
1055
|
+
}
|
|
1056
|
+
// Get all entries (excluding snapshot entries - we'll handle delete separately)
|
|
1057
|
+
// Include "doc_create", "doc_change", and "doc_delete" types as they all contain Automerge changes to apply
|
|
1058
|
+
const entriesToLoad = startFromSnapshot
|
|
1059
|
+
? allEntryMetadata.filter(em => (em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete") && em.createdAt > snapshotMeta.createdAt)
|
|
1060
|
+
: allEntryMetadata.filter(em => em.entryType === "doc_create" || em.entryType === "doc_change" || em.entryType === "doc_delete");
|
|
1061
|
+
console.log(`[BaseMindooDB] Will load ${entriesToLoad.length} entries for document ${docId} (after snapshot filter)`);
|
|
1062
|
+
// Load the snapshot first if we have one
|
|
1063
|
+
let doc = undefined;
|
|
1064
|
+
if (startFromSnapshot && snapshotMeta) {
|
|
1065
|
+
console.log(`[BaseMindooDB] Loading snapshot for document ${docId}`);
|
|
1066
|
+
const snapshotEntries = await this.store.getEntries([snapshotMeta.id]);
|
|
1067
|
+
console.log(`[BaseMindooDB] Retrieved ${snapshotEntries.length} snapshot entry(s) from store`);
|
|
1068
|
+
if (snapshotEntries.length > 0) {
|
|
1069
|
+
const snapshotData = snapshotEntries[0];
|
|
1070
|
+
// Admin-only mode: only accept snapshots signed by admin
|
|
1071
|
+
let isValid = false;
|
|
1072
|
+
if (this._isAdminOnlyDb && snapshotData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1073
|
+
console.warn(`[BaseMindooDB] Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
// Verify signature against the encrypted snapshot (no decryption needed)
|
|
1077
|
+
// We sign the encrypted payload, so anyone can verify signatures without decryption keys
|
|
1078
|
+
isValid = await this.tenant.verifySignature(snapshotData.encryptedData, snapshotData.signature, snapshotData.createdByPublicKey);
|
|
1079
|
+
}
|
|
1080
|
+
if (!isValid) {
|
|
1081
|
+
console.warn(`[BaseMindooDB] Invalid signature for snapshot ${snapshotData.id}, falling back to loading from scratch`);
|
|
1082
|
+
// Fall back to loading from scratch
|
|
1083
|
+
startFromSnapshot = false;
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
console.log(`[BaseMindooDB] Snapshot signature valid, decrypting snapshot`);
|
|
1087
|
+
// Decrypt snapshot (only after signature verification passes)
|
|
1088
|
+
const decryptedSnapshot = await this.tenant.decryptPayload(snapshotData.encryptedData, snapshotData.decryptionKeyId);
|
|
1089
|
+
console.log(`[BaseMindooDB] Decrypted snapshot (${snapshotData.encryptedData.length} -> ${decryptedSnapshot.length} bytes)`);
|
|
1090
|
+
// Load snapshot using Automerge.load()
|
|
1091
|
+
// This deserializes a full document snapshot from binary data
|
|
1092
|
+
// According to Automerge docs: load() is equivalent to init() followed by loadIncremental()
|
|
1093
|
+
console.log(`[BaseMindooDB] Loading snapshot into Automerge document`);
|
|
1094
|
+
doc = Automerge.load(decryptedSnapshot);
|
|
1095
|
+
console.log(`[BaseMindooDB] Successfully loaded snapshot, document heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
1096
|
+
// Register the snapshot's automerge hash -> entry ID mapping
|
|
1097
|
+
const parsed = (0, idGeneration_1.parseDocEntryId)(snapshotData.id);
|
|
1098
|
+
if (parsed) {
|
|
1099
|
+
this.registerAutomergeHashMapping(docId, parsed.automergeHash, snapshotData.id);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// If we don't have a snapshot, start from scratch
|
|
1105
|
+
if (!doc) {
|
|
1106
|
+
console.log(`[BaseMindooDB] Initializing new Automerge document for ${docId}`);
|
|
1107
|
+
doc = Automerge.init();
|
|
1108
|
+
console.log(`[BaseMindooDB] Initialized empty document, heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
1109
|
+
}
|
|
1110
|
+
// Sort entries by timestamp
|
|
1111
|
+
entriesToLoad.sort((a, b) => a.createdAt - b.createdAt);
|
|
1112
|
+
console.log(`[BaseMindooDB] Sorted ${entriesToLoad.length} entries by timestamp for document ${docId}`);
|
|
1113
|
+
// Load and apply all entries
|
|
1114
|
+
console.log(`[BaseMindooDB] Fetching ${entriesToLoad.length} entries from store for document ${docId}`);
|
|
1115
|
+
const entries = await this.store.getEntries(entriesToLoad.map(em => em.id));
|
|
1116
|
+
console.log(`[BaseMindooDB] Retrieved ${entries.length} entries from store for document ${docId}`);
|
|
1117
|
+
console.log(`[BaseMindooDB] Loading document ${docId}: found ${entries.length} entries to apply (${startFromSnapshot ? 'starting from snapshot' : 'starting from scratch'})`);
|
|
1118
|
+
// Log current document state before applying entries
|
|
1119
|
+
console.log(`[BaseMindooDB] Document state before applying entries: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
1120
|
+
// Verify signatures, decrypt, and apply entries one at a time
|
|
1121
|
+
// This ensures dependencies are handled correctly by Automerge
|
|
1122
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1123
|
+
const entryData = entries[i];
|
|
1124
|
+
console.log(`[BaseMindooDB] ===== Processing entry ${i + 1}/${entries.length} for document ${docId} =====`);
|
|
1125
|
+
console.log(`[BaseMindooDB] Entry id: ${entryData.id}`);
|
|
1126
|
+
console.log(`[BaseMindooDB] Entry type: ${entryData.entryType}`);
|
|
1127
|
+
console.log(`[BaseMindooDB] Entry createdAt: ${entryData.createdAt}`);
|
|
1128
|
+
console.log(`[BaseMindooDB] Entry dependencies: ${JSON.stringify(entryData.dependencyIds || [])}`);
|
|
1129
|
+
console.log(`[BaseMindooDB] Entry payload size: ${entryData.encryptedData.length} bytes`);
|
|
1130
|
+
// Admin-only mode: only accept entries signed by the admin key
|
|
1131
|
+
// This prevents recursion and ensures security for the directory database
|
|
1132
|
+
if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1133
|
+
console.warn(`[BaseMindooDB] Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
// Verify signature against the encrypted payload (no decryption needed)
|
|
1137
|
+
// We sign the encrypted payload, so anyone can verify signatures without decryption keys
|
|
1138
|
+
console.log(`[BaseMindooDB] Verifying signature for entry ${entryData.id}`);
|
|
1139
|
+
const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
|
|
1140
|
+
if (!isValid) {
|
|
1141
|
+
console.warn(`[BaseMindooDB] Invalid signature for entry ${entryData.id}, skipping`);
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
console.log(`[BaseMindooDB] Signature valid for entry ${entryData.id}`);
|
|
1145
|
+
// Decrypt payload (only after signature verification passes)
|
|
1146
|
+
console.log(`[BaseMindooDB] Decrypting payload for entry ${entryData.id} with key ${entryData.decryptionKeyId}`);
|
|
1147
|
+
const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
|
|
1148
|
+
console.log(`[BaseMindooDB] Decrypted payload: ${entryData.encryptedData.length} -> ${decryptedPayload.length} bytes`);
|
|
1149
|
+
// Apply change using applyChanges with raw change bytes
|
|
1150
|
+
// applyChanges expects an array of Uint8Array (raw change bytes)
|
|
1151
|
+
try {
|
|
1152
|
+
console.log(`[BaseMindooDB] Document state before applying entry ${i + 1}: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
1153
|
+
console.log(`[BaseMindooDB] Calling Automerge.applyChanges for entry ${i + 1}/${entries.length} on document ${docId}`);
|
|
1154
|
+
console.log(`[BaseMindooDB] Decrypted payload length: ${decryptedPayload.length} bytes`);
|
|
1155
|
+
const currentDoc = doc;
|
|
1156
|
+
const result = Automerge.applyChanges(currentDoc, [decryptedPayload]);
|
|
1157
|
+
doc = result[0];
|
|
1158
|
+
console.log(`[BaseMindooDB] Successfully applied entry ${i + 1}/${entries.length}`);
|
|
1159
|
+
console.log(`[BaseMindooDB] Document state after applying entry ${i + 1}: heads=${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
1160
|
+
// Register the automerge hash -> entry ID mapping for future dependency resolution
|
|
1161
|
+
const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
|
|
1162
|
+
if (parsed) {
|
|
1163
|
+
this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
console.error(`[BaseMindooDB] Error applying entry ${entryData.id} to document ${docId}:`, error);
|
|
1168
|
+
console.error(`[BaseMindooDB] Entry index: ${i + 1}/${entries.length}`);
|
|
1169
|
+
console.error(`[BaseMindooDB] Entry dependencies:`, entryData.dependencyIds);
|
|
1170
|
+
console.error(`[BaseMindooDB] Entry type:`, entryData.entryType);
|
|
1171
|
+
console.error(`[BaseMindooDB] Entry createdAt:`, entryData.createdAt);
|
|
1172
|
+
console.error(`[BaseMindooDB] Document state before entry:`, {
|
|
1173
|
+
hasDoc: !!doc,
|
|
1174
|
+
docHeads: doc ? Automerge.getHeads(doc) : null,
|
|
1175
|
+
});
|
|
1176
|
+
// Log previous entry for debugging dependency issues
|
|
1177
|
+
if (i > 0) {
|
|
1178
|
+
const prevEntry = entries[i - 1];
|
|
1179
|
+
console.error(`[BaseMindooDB] Previous entry:`, {
|
|
1180
|
+
id: prevEntry.id,
|
|
1181
|
+
deps: prevEntry.dependencyIds,
|
|
1182
|
+
type: prevEntry.entryType,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
throw error;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
// Extract metadata from document (doc is guaranteed to be defined at this point)
|
|
1189
|
+
console.log(`[BaseMindooDB] All entries applied successfully for document ${docId}`);
|
|
1190
|
+
console.log(`[BaseMindooDB] Final document heads: ${JSON.stringify(Automerge.getHeads(doc))}`);
|
|
1191
|
+
const payload = doc;
|
|
1192
|
+
// Check if document was deleted by looking for a "doc_delete" type entry
|
|
1193
|
+
const hasDeleteEntry = allEntryMetadata.some(em => em.entryType === "doc_delete");
|
|
1194
|
+
const isDeleted = hasDeleteEntry;
|
|
1195
|
+
console.log(`[BaseMindooDB] Document ${docId} isDeleted: ${isDeleted}`);
|
|
1196
|
+
const decryptionKeyId = payload._decryptionKeyId || "default";
|
|
1197
|
+
// Get lastModified from payload, or use the timestamp of the last entry
|
|
1198
|
+
const lastEntry = entries.length > 0 ? entries[entries.length - 1] : null;
|
|
1199
|
+
const lastModified = payload._lastModified ||
|
|
1200
|
+
(lastEntry ? lastEntry.createdAt : Date.now());
|
|
1201
|
+
// Get createdAt from the first entry
|
|
1202
|
+
const firstEntry = allEntryMetadata.length > 0 ? allEntryMetadata[0] : null;
|
|
1203
|
+
const createdAt = firstEntry ? firstEntry.createdAt : lastModified;
|
|
1204
|
+
console.log(`[BaseMindooDB] Document ${docId} metadata: createdAt=${createdAt}, lastModified=${lastModified}, decryptionKeyId=${decryptionKeyId}`);
|
|
1205
|
+
const internalDoc = {
|
|
1206
|
+
id: docId,
|
|
1207
|
+
doc: doc, // doc is guaranteed to be defined at this point
|
|
1208
|
+
createdAt,
|
|
1209
|
+
lastModified,
|
|
1210
|
+
decryptionKeyId,
|
|
1211
|
+
isDeleted,
|
|
1212
|
+
};
|
|
1213
|
+
// Update cache
|
|
1214
|
+
this.docCache.set(docId, internalDoc);
|
|
1215
|
+
console.log(`[BaseMindooDB] ===== Successfully loaded document ${docId} and cached it =====`);
|
|
1216
|
+
return internalDoc;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Wrap an internal document in the MindooDoc interface.
|
|
1220
|
+
* This is the read-only wrapper - write methods throw errors.
|
|
1221
|
+
*/
|
|
1222
|
+
wrapDocument(internalDoc) {
|
|
1223
|
+
const db = this;
|
|
1224
|
+
const docId = internalDoc.id;
|
|
1225
|
+
// Create a read-only proxy that throws on any modification attempts
|
|
1226
|
+
const createReadOnlyProxy = (target) => {
|
|
1227
|
+
return new Proxy(target, {
|
|
1228
|
+
set: (_target, prop) => {
|
|
1229
|
+
throw new Error(`Cannot modify property '${String(prop)}' on read-only document. Use changeDoc() to modify documents.`);
|
|
1230
|
+
},
|
|
1231
|
+
deleteProperty: (_target, prop) => {
|
|
1232
|
+
throw new Error(`Cannot delete property '${String(prop)}' on read-only document. Use changeDoc() to modify documents.`);
|
|
1233
|
+
},
|
|
1234
|
+
get: (target, prop) => {
|
|
1235
|
+
const value = target[prop];
|
|
1236
|
+
// Recursively wrap nested objects (but not arrays or special types)
|
|
1237
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Uint8Array)) {
|
|
1238
|
+
return createReadOnlyProxy(value);
|
|
1239
|
+
}
|
|
1240
|
+
return value;
|
|
1241
|
+
},
|
|
1242
|
+
});
|
|
1243
|
+
};
|
|
1244
|
+
return {
|
|
1245
|
+
getDatabase: () => db,
|
|
1246
|
+
getId: () => docId,
|
|
1247
|
+
getCreatedAt: () => internalDoc.createdAt,
|
|
1248
|
+
getLastModified: () => internalDoc.lastModified,
|
|
1249
|
+
isDeleted: () => internalDoc.isDeleted,
|
|
1250
|
+
getData: () => createReadOnlyProxy(internalDoc.doc),
|
|
1251
|
+
// ========== Attachment Write Methods ==========
|
|
1252
|
+
// These throw errors in the read-only wrapper
|
|
1253
|
+
addAttachment: async () => {
|
|
1254
|
+
throw new Error("addAttachment() can only be called within changeDoc() callback");
|
|
1255
|
+
},
|
|
1256
|
+
addAttachmentStream: async () => {
|
|
1257
|
+
throw new Error("addAttachmentStream() can only be called within changeDoc() callback");
|
|
1258
|
+
},
|
|
1259
|
+
removeAttachment: async () => {
|
|
1260
|
+
throw new Error("removeAttachment() can only be called within changeDoc() callback");
|
|
1261
|
+
},
|
|
1262
|
+
appendToAttachment: async () => {
|
|
1263
|
+
throw new Error("appendToAttachment() can only be called within changeDoc() callback");
|
|
1264
|
+
},
|
|
1265
|
+
// ========== Attachment Read Methods ==========
|
|
1266
|
+
// These work in the read-only wrapper
|
|
1267
|
+
getAttachments: () => {
|
|
1268
|
+
const payload = internalDoc.doc;
|
|
1269
|
+
return payload._attachments || [];
|
|
1270
|
+
},
|
|
1271
|
+
getAttachment: async (attachmentId) => {
|
|
1272
|
+
return db.getAttachmentInternal(docId, attachmentId);
|
|
1273
|
+
},
|
|
1274
|
+
getAttachmentRange: async (attachmentId, startByte, endByte) => {
|
|
1275
|
+
return db.getAttachmentRangeInternal(docId, attachmentId, startByte, endByte);
|
|
1276
|
+
},
|
|
1277
|
+
streamAttachment: (attachmentId, startOffset = 0) => {
|
|
1278
|
+
return db.streamAttachmentInternal(docId, attachmentId, startOffset);
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Get an attachment reference by ID from a document's _attachments array.
|
|
1284
|
+
*/
|
|
1285
|
+
getAttachmentRefInternal(docId, attachmentId) {
|
|
1286
|
+
const internalDoc = this.docCache.get(docId);
|
|
1287
|
+
if (!internalDoc) {
|
|
1288
|
+
throw new Error(`Document ${docId} not found in cache`);
|
|
1289
|
+
}
|
|
1290
|
+
const payload = internalDoc.doc;
|
|
1291
|
+
const attachments = payload._attachments || [];
|
|
1292
|
+
const ref = attachments.find(a => a.attachmentId === attachmentId);
|
|
1293
|
+
if (!ref) {
|
|
1294
|
+
throw new Error(`Attachment ${attachmentId} not found in document ${docId}`);
|
|
1295
|
+
}
|
|
1296
|
+
return ref;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Internal method to fetch and concatenate all chunks for an attachment.
|
|
1300
|
+
*/
|
|
1301
|
+
async getAttachmentInternal(docId, attachmentId) {
|
|
1302
|
+
console.log(`[BaseMindooDB] Getting attachment ${attachmentId} from document ${docId}`);
|
|
1303
|
+
const ref = this.getAttachmentRefInternal(docId, attachmentId);
|
|
1304
|
+
const store = this.getEffectiveAttachmentStore();
|
|
1305
|
+
// Resolve dependency chain to get all chunk IDs in order (oldest first)
|
|
1306
|
+
const chunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
|
|
1307
|
+
console.log(`[BaseMindooDB] Resolved ${chunkIds.length} chunks for attachment ${attachmentId}`);
|
|
1308
|
+
// Fetch all chunks
|
|
1309
|
+
const chunks = await store.getEntries(chunkIds);
|
|
1310
|
+
// Verify signatures, decrypt, and collect plaintext chunks
|
|
1311
|
+
const plaintextChunks = [];
|
|
1312
|
+
let totalSize = 0;
|
|
1313
|
+
for (const chunk of chunks) {
|
|
1314
|
+
// Admin-only mode: only accept chunks signed by the admin key
|
|
1315
|
+
if (this._isAdminOnlyDb && chunk.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1316
|
+
throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
|
|
1317
|
+
}
|
|
1318
|
+
// Verify signature
|
|
1319
|
+
const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
|
|
1320
|
+
if (!isValid) {
|
|
1321
|
+
throw new Error(`Invalid signature for chunk ${chunk.id}`);
|
|
1322
|
+
}
|
|
1323
|
+
// Decrypt
|
|
1324
|
+
const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
|
|
1325
|
+
plaintextChunks.push(plaintext);
|
|
1326
|
+
totalSize += plaintext.length;
|
|
1327
|
+
}
|
|
1328
|
+
// Concatenate all chunks into final result
|
|
1329
|
+
const result = new Uint8Array(totalSize);
|
|
1330
|
+
let offset = 0;
|
|
1331
|
+
for (const chunk of plaintextChunks) {
|
|
1332
|
+
result.set(chunk, offset);
|
|
1333
|
+
offset += chunk.length;
|
|
1334
|
+
}
|
|
1335
|
+
console.log(`[BaseMindooDB] Retrieved attachment ${attachmentId}: ${result.length} bytes`);
|
|
1336
|
+
return result;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Internal method to get a byte range from an attachment.
|
|
1340
|
+
*/
|
|
1341
|
+
async getAttachmentRangeInternal(docId, attachmentId, startByte, endByte) {
|
|
1342
|
+
console.log(`[BaseMindooDB] Getting attachment ${attachmentId} range [${startByte}, ${endByte}) from document ${docId}`);
|
|
1343
|
+
if (startByte < 0 || endByte <= startByte) {
|
|
1344
|
+
throw new Error(`Invalid byte range: [${startByte}, ${endByte})`);
|
|
1345
|
+
}
|
|
1346
|
+
const ref = this.getAttachmentRefInternal(docId, attachmentId);
|
|
1347
|
+
if (endByte > ref.size) {
|
|
1348
|
+
throw new Error(`End byte ${endByte} exceeds attachment size ${ref.size}`);
|
|
1349
|
+
}
|
|
1350
|
+
const store = this.getEffectiveAttachmentStore();
|
|
1351
|
+
const chunkSize = this.chunkSizeBytes;
|
|
1352
|
+
// Calculate which chunks we need
|
|
1353
|
+
const startChunkIndex = Math.floor(startByte / chunkSize);
|
|
1354
|
+
const endChunkIndex = Math.floor((endByte - 1) / chunkSize);
|
|
1355
|
+
// Resolve dependency chain to get all chunk IDs
|
|
1356
|
+
const allChunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
|
|
1357
|
+
// Get only the chunks we need
|
|
1358
|
+
const neededChunkIds = allChunkIds.slice(startChunkIndex, endChunkIndex + 1);
|
|
1359
|
+
const chunks = await store.getEntries(neededChunkIds);
|
|
1360
|
+
// Decrypt needed chunks
|
|
1361
|
+
const plaintextChunks = [];
|
|
1362
|
+
for (const chunk of chunks) {
|
|
1363
|
+
// Admin-only mode: only accept chunks signed by the admin key
|
|
1364
|
+
if (this._isAdminOnlyDb && chunk.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1365
|
+
throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
|
|
1366
|
+
}
|
|
1367
|
+
// Verify signature
|
|
1368
|
+
const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
|
|
1369
|
+
if (!isValid) {
|
|
1370
|
+
throw new Error(`Invalid signature for chunk ${chunk.id}`);
|
|
1371
|
+
}
|
|
1372
|
+
// Decrypt
|
|
1373
|
+
const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
|
|
1374
|
+
plaintextChunks.push(plaintext);
|
|
1375
|
+
}
|
|
1376
|
+
// Calculate offsets within the fetched chunks
|
|
1377
|
+
const offsetInFirstChunk = startByte - (startChunkIndex * chunkSize);
|
|
1378
|
+
const totalNeededBytes = endByte - startByte;
|
|
1379
|
+
// Extract the requested range
|
|
1380
|
+
const result = new Uint8Array(totalNeededBytes);
|
|
1381
|
+
let resultOffset = 0;
|
|
1382
|
+
let bytesRemaining = totalNeededBytes;
|
|
1383
|
+
for (let i = 0; i < plaintextChunks.length && bytesRemaining > 0; i++) {
|
|
1384
|
+
const chunk = plaintextChunks[i];
|
|
1385
|
+
const chunkStart = i === 0 ? offsetInFirstChunk : 0;
|
|
1386
|
+
const bytesToCopy = Math.min(chunk.length - chunkStart, bytesRemaining);
|
|
1387
|
+
result.set(chunk.slice(chunkStart, chunkStart + bytesToCopy), resultOffset);
|
|
1388
|
+
resultOffset += bytesToCopy;
|
|
1389
|
+
bytesRemaining -= bytesToCopy;
|
|
1390
|
+
}
|
|
1391
|
+
console.log(`[BaseMindooDB] Retrieved attachment ${attachmentId} range: ${result.length} bytes`);
|
|
1392
|
+
return result;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Internal async generator to stream attachment data.
|
|
1396
|
+
*/
|
|
1397
|
+
async *streamAttachmentInternal(docId, attachmentId, startOffset) {
|
|
1398
|
+
console.log(`[BaseMindooDB] Streaming attachment ${attachmentId} from offset ${startOffset}`);
|
|
1399
|
+
const ref = this.getAttachmentRefInternal(docId, attachmentId);
|
|
1400
|
+
const store = this.getEffectiveAttachmentStore();
|
|
1401
|
+
const chunkSize = this.chunkSizeBytes;
|
|
1402
|
+
// Calculate starting chunk
|
|
1403
|
+
const startChunkIndex = Math.floor(startOffset / chunkSize);
|
|
1404
|
+
const offsetInStartChunk = startOffset % chunkSize;
|
|
1405
|
+
// Resolve dependency chain to get all chunk IDs
|
|
1406
|
+
const allChunkIds = await store.resolveDependencies(ref.lastChunkId, { includeStart: true });
|
|
1407
|
+
// Stream chunks starting from startChunkIndex
|
|
1408
|
+
for (let i = startChunkIndex; i < allChunkIds.length; i++) {
|
|
1409
|
+
const [chunk] = await store.getEntries([allChunkIds[i]]);
|
|
1410
|
+
// Admin-only mode: only accept chunks signed by the admin key
|
|
1411
|
+
if (this._isAdminOnlyDb && chunk.createdByPublicKey !== this.getAdminPublicKey()) {
|
|
1412
|
+
throw new Error(`Admin-only DB: chunk ${chunk.id} not signed by admin key`);
|
|
1413
|
+
}
|
|
1414
|
+
// Verify signature
|
|
1415
|
+
const isValid = await this.tenant.verifySignature(chunk.encryptedData, chunk.signature, chunk.createdByPublicKey);
|
|
1416
|
+
if (!isValid) {
|
|
1417
|
+
throw new Error(`Invalid signature for chunk ${chunk.id}`);
|
|
1418
|
+
}
|
|
1419
|
+
// Decrypt
|
|
1420
|
+
const plaintext = await this.tenant.decryptAttachmentPayload(chunk.encryptedData, chunk.decryptionKeyId);
|
|
1421
|
+
// For first chunk, skip bytes before startOffset
|
|
1422
|
+
if (i === startChunkIndex && offsetInStartChunk > 0) {
|
|
1423
|
+
yield plaintext.slice(offsetInStartChunk);
|
|
1424
|
+
}
|
|
1425
|
+
else {
|
|
1426
|
+
yield plaintext;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
console.log(`[BaseMindooDB] Finished streaming attachment ${attachmentId}`);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Get the effective attachment store (attachment store if configured, otherwise doc store).
|
|
1433
|
+
*/
|
|
1434
|
+
getEffectiveAttachmentStore() {
|
|
1435
|
+
return this.attachmentStore || this.store;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Internal method to add an attachment by chunking the file and storing chunks.
|
|
1439
|
+
*/
|
|
1440
|
+
async addAttachmentInternal(docId, fileData, fileName, mimeType, decryptionKeyId, createdAt) {
|
|
1441
|
+
console.log(`[BaseMindooDB] Adding attachment to document ${docId}: ${fileName} (${fileData.length} bytes)`);
|
|
1442
|
+
const store = this.getEffectiveAttachmentStore();
|
|
1443
|
+
const currentUser = await this.tenant.getCurrentUserId();
|
|
1444
|
+
const attachmentId = (0, idGeneration_1.generateFileUuid7)();
|
|
1445
|
+
// Chunk the file
|
|
1446
|
+
const chunks = [];
|
|
1447
|
+
let prevChunkId = null;
|
|
1448
|
+
let lastChunkId = "";
|
|
1449
|
+
for (let offset = 0; offset < fileData.length; offset += this.chunkSizeBytes) {
|
|
1450
|
+
const chunkData = fileData.slice(offset, Math.min(offset + this.chunkSizeBytes, fileData.length));
|
|
1451
|
+
// Encrypt chunk
|
|
1452
|
+
const encryptedData = await this.tenant.encryptAttachmentPayload(chunkData, decryptionKeyId);
|
|
1453
|
+
// Compute content hash
|
|
1454
|
+
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedData, this.getSubtle());
|
|
1455
|
+
// Generate chunk ID
|
|
1456
|
+
const chunkId = (0, idGeneration_1.generateAttachmentChunkId)(docId, attachmentId);
|
|
1457
|
+
lastChunkId = chunkId;
|
|
1458
|
+
// Sign the encrypted chunk
|
|
1459
|
+
const signature = await this.tenant.signPayload(encryptedData);
|
|
1460
|
+
// Create chunk entry
|
|
1461
|
+
const chunkEntry = {
|
|
1462
|
+
entryType: "attachment_chunk",
|
|
1463
|
+
id: chunkId,
|
|
1464
|
+
contentHash,
|
|
1465
|
+
docId,
|
|
1466
|
+
dependencyIds: prevChunkId ? [prevChunkId] : [],
|
|
1467
|
+
createdAt,
|
|
1468
|
+
createdByPublicKey: currentUser.userSigningPublicKey,
|
|
1469
|
+
decryptionKeyId,
|
|
1470
|
+
signature,
|
|
1471
|
+
originalSize: chunkData.length,
|
|
1472
|
+
encryptedSize: encryptedData.length,
|
|
1473
|
+
encryptedData,
|
|
1474
|
+
};
|
|
1475
|
+
chunks.push(chunkEntry);
|
|
1476
|
+
prevChunkId = chunkId;
|
|
1477
|
+
}
|
|
1478
|
+
// Store all chunks
|
|
1479
|
+
await store.putEntries(chunks);
|
|
1480
|
+
console.log(`[BaseMindooDB] Stored ${chunks.length} chunks for attachment ${attachmentId}`);
|
|
1481
|
+
// Create attachment reference
|
|
1482
|
+
const ref = {
|
|
1483
|
+
attachmentId,
|
|
1484
|
+
fileName,
|
|
1485
|
+
mimeType,
|
|
1486
|
+
size: fileData.length,
|
|
1487
|
+
lastChunkId,
|
|
1488
|
+
decryptionKeyId,
|
|
1489
|
+
createdAt,
|
|
1490
|
+
createdBy: currentUser.userSigningPublicKey,
|
|
1491
|
+
};
|
|
1492
|
+
return ref;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Internal method to add an attachment from a streaming data source.
|
|
1496
|
+
* Memory efficient - processes data chunk by chunk without loading entire file into memory.
|
|
1497
|
+
*/
|
|
1498
|
+
async addAttachmentStreamInternal(docId, dataStream, fileName, mimeType, decryptionKeyId, createdAt) {
|
|
1499
|
+
console.log(`[BaseMindooDB] Adding streaming attachment to document ${docId}: ${fileName}`);
|
|
1500
|
+
const store = this.getEffectiveAttachmentStore();
|
|
1501
|
+
const currentUser = await this.tenant.getCurrentUserId();
|
|
1502
|
+
const attachmentId = (0, idGeneration_1.generateFileUuid7)();
|
|
1503
|
+
// Buffer to accumulate incoming data until we have a full chunk
|
|
1504
|
+
let buffer = new Uint8Array(0);
|
|
1505
|
+
let totalSize = 0;
|
|
1506
|
+
let prevChunkId = null;
|
|
1507
|
+
let lastChunkId = "";
|
|
1508
|
+
let chunkCount = 0;
|
|
1509
|
+
// Helper to concatenate Uint8Arrays
|
|
1510
|
+
const concatArrays = (a, b) => {
|
|
1511
|
+
const result = new Uint8Array(a.length + b.length);
|
|
1512
|
+
result.set(a, 0);
|
|
1513
|
+
result.set(b, a.length);
|
|
1514
|
+
return result;
|
|
1515
|
+
};
|
|
1516
|
+
// Helper to store a chunk
|
|
1517
|
+
const storeChunk = async (chunkData) => {
|
|
1518
|
+
// Encrypt chunk
|
|
1519
|
+
const encryptedData = await this.tenant.encryptAttachmentPayload(chunkData, decryptionKeyId);
|
|
1520
|
+
// Compute content hash
|
|
1521
|
+
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedData, this.getSubtle());
|
|
1522
|
+
// Generate chunk ID
|
|
1523
|
+
const chunkId = (0, idGeneration_1.generateAttachmentChunkId)(docId, attachmentId);
|
|
1524
|
+
// Sign the encrypted chunk
|
|
1525
|
+
const signature = await this.tenant.signPayload(encryptedData);
|
|
1526
|
+
// Create chunk entry
|
|
1527
|
+
const chunkEntry = {
|
|
1528
|
+
entryType: "attachment_chunk",
|
|
1529
|
+
id: chunkId,
|
|
1530
|
+
contentHash,
|
|
1531
|
+
docId,
|
|
1532
|
+
dependencyIds: prevChunkId ? [prevChunkId] : [],
|
|
1533
|
+
createdAt,
|
|
1534
|
+
createdByPublicKey: currentUser.userSigningPublicKey,
|
|
1535
|
+
decryptionKeyId,
|
|
1536
|
+
signature,
|
|
1537
|
+
originalSize: chunkData.length,
|
|
1538
|
+
encryptedSize: encryptedData.length,
|
|
1539
|
+
encryptedData,
|
|
1540
|
+
};
|
|
1541
|
+
// Store immediately (streaming - don't buffer chunks in memory)
|
|
1542
|
+
await store.putEntries([chunkEntry]);
|
|
1543
|
+
chunkCount++;
|
|
1544
|
+
return chunkId;
|
|
1545
|
+
};
|
|
1546
|
+
// Process incoming data stream
|
|
1547
|
+
for await (const chunk of dataStream) {
|
|
1548
|
+
// Add incoming data to buffer
|
|
1549
|
+
buffer = concatArrays(buffer, chunk);
|
|
1550
|
+
// Process complete chunks
|
|
1551
|
+
while (buffer.length >= this.chunkSizeBytes) {
|
|
1552
|
+
const chunkData = buffer.slice(0, this.chunkSizeBytes);
|
|
1553
|
+
buffer = buffer.slice(this.chunkSizeBytes);
|
|
1554
|
+
lastChunkId = await storeChunk(chunkData);
|
|
1555
|
+
prevChunkId = lastChunkId;
|
|
1556
|
+
totalSize += chunkData.length;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
// Store remaining data as final chunk (if any)
|
|
1560
|
+
if (buffer.length > 0) {
|
|
1561
|
+
lastChunkId = await storeChunk(buffer);
|
|
1562
|
+
totalSize += buffer.length;
|
|
1563
|
+
}
|
|
1564
|
+
console.log(`[BaseMindooDB] Stored ${chunkCount} chunks for streaming attachment ${attachmentId} (${totalSize} bytes)`);
|
|
1565
|
+
// Create attachment reference
|
|
1566
|
+
const ref = {
|
|
1567
|
+
attachmentId,
|
|
1568
|
+
fileName,
|
|
1569
|
+
mimeType,
|
|
1570
|
+
size: totalSize,
|
|
1571
|
+
lastChunkId,
|
|
1572
|
+
decryptionKeyId,
|
|
1573
|
+
createdAt,
|
|
1574
|
+
createdBy: currentUser.userSigningPublicKey,
|
|
1575
|
+
};
|
|
1576
|
+
return ref;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Internal method to append data to an existing attachment.
|
|
1580
|
+
*/
|
|
1581
|
+
async appendToAttachmentInternal(docId, attachmentId, decryptionKeyId, prevLastChunkId, data, createdAt) {
|
|
1582
|
+
console.log(`[BaseMindooDB] Appending ${data.length} bytes to attachment ${attachmentId}`);
|
|
1583
|
+
const store = this.getEffectiveAttachmentStore();
|
|
1584
|
+
const currentUser = await this.tenant.getCurrentUserId();
|
|
1585
|
+
// Chunk the data
|
|
1586
|
+
const chunks = [];
|
|
1587
|
+
let prevChunkId = prevLastChunkId;
|
|
1588
|
+
let lastChunkId = prevLastChunkId;
|
|
1589
|
+
for (let offset = 0; offset < data.length; offset += this.chunkSizeBytes) {
|
|
1590
|
+
const chunkData = data.slice(offset, Math.min(offset + this.chunkSizeBytes, data.length));
|
|
1591
|
+
// Encrypt chunk
|
|
1592
|
+
const encryptedData = await this.tenant.encryptAttachmentPayload(chunkData, decryptionKeyId);
|
|
1593
|
+
// Compute content hash
|
|
1594
|
+
const contentHash = await (0, idGeneration_1.computeContentHash)(encryptedData, this.getSubtle());
|
|
1595
|
+
// Generate chunk ID
|
|
1596
|
+
const chunkId = (0, idGeneration_1.generateAttachmentChunkId)(docId, attachmentId);
|
|
1597
|
+
lastChunkId = chunkId;
|
|
1598
|
+
// Sign the encrypted chunk
|
|
1599
|
+
const signature = await this.tenant.signPayload(encryptedData);
|
|
1600
|
+
// Create chunk entry with dependency on previous chunk
|
|
1601
|
+
const chunkEntry = {
|
|
1602
|
+
entryType: "attachment_chunk",
|
|
1603
|
+
id: chunkId,
|
|
1604
|
+
contentHash,
|
|
1605
|
+
docId,
|
|
1606
|
+
dependencyIds: [prevChunkId],
|
|
1607
|
+
createdAt,
|
|
1608
|
+
createdByPublicKey: currentUser.userSigningPublicKey,
|
|
1609
|
+
decryptionKeyId,
|
|
1610
|
+
signature,
|
|
1611
|
+
originalSize: chunkData.length,
|
|
1612
|
+
encryptedSize: encryptedData.length,
|
|
1613
|
+
encryptedData,
|
|
1614
|
+
};
|
|
1615
|
+
chunks.push(chunkEntry);
|
|
1616
|
+
prevChunkId = chunkId;
|
|
1617
|
+
}
|
|
1618
|
+
// Store all chunks
|
|
1619
|
+
await store.putEntries(chunks);
|
|
1620
|
+
console.log(`[BaseMindooDB] Appended ${chunks.length} chunks to attachment ${attachmentId}`);
|
|
1621
|
+
return {
|
|
1622
|
+
lastChunkId,
|
|
1623
|
+
sizeIncrease: data.length,
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Pull changes from a remote content-addressed store.
|
|
1628
|
+
*
|
|
1629
|
+
* This method:
|
|
1630
|
+
* 1. Finds entries in the remote store that we don't have locally
|
|
1631
|
+
* 2. Retrieves those entries from the remote store
|
|
1632
|
+
* 3. Stores them in our local store
|
|
1633
|
+
* 4. Syncs the local store to process the new entries
|
|
1634
|
+
*
|
|
1635
|
+
* @param remoteStore The remote store to pull entries from
|
|
1636
|
+
* @return A promise that resolves when the pull is complete
|
|
1637
|
+
*/
|
|
1638
|
+
async pullChangesFrom(remoteStore) {
|
|
1639
|
+
if (this.store.getId() !== remoteStore.getId()) {
|
|
1640
|
+
throw new Error(`[BaseMindooDB] Cannot pull entries from the incompatible store ${this.store.getId()}`);
|
|
1641
|
+
}
|
|
1642
|
+
console.log(`[BaseMindooDB] Pulling entries from remote store ${remoteStore.getId()}`);
|
|
1643
|
+
// Get all entry IDs we already have in our local store
|
|
1644
|
+
const localEntryIds = await this.store.getAllIds();
|
|
1645
|
+
console.log(`[BaseMindooDB] Local store has ${localEntryIds.length} entry IDs`);
|
|
1646
|
+
// Find entries in the remote store that we don't have
|
|
1647
|
+
const newEntryMetadata = await remoteStore.findNewEntries(localEntryIds);
|
|
1648
|
+
console.log(`[BaseMindooDB] Found ${newEntryMetadata.length} new entries in remote store`);
|
|
1649
|
+
if (newEntryMetadata.length === 0) {
|
|
1650
|
+
console.log(`[BaseMindooDB] No new entries to pull`);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
// Get the full entries from the remote store
|
|
1654
|
+
const newEntries = await remoteStore.getEntries(newEntryMetadata.map(em => em.id));
|
|
1655
|
+
console.log(`[BaseMindooDB] Retrieved ${newEntries.length} entries from remote store`);
|
|
1656
|
+
// Store all entries in our local store
|
|
1657
|
+
// The putEntries method handles deduplication (no-op if entry already exists)
|
|
1658
|
+
await this.store.putEntries(newEntries);
|
|
1659
|
+
console.log(`[BaseMindooDB] Stored ${newEntries.length} entries in local store`);
|
|
1660
|
+
// Sync the local store to process the new entries
|
|
1661
|
+
// This will update the index, cache, and processedEntryIds
|
|
1662
|
+
await this.syncStoreChanges();
|
|
1663
|
+
console.log(`[BaseMindooDB] Pull complete, synced ${newEntries.length} entries`);
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Push changes to a remote content-addressed store.
|
|
1667
|
+
*
|
|
1668
|
+
* This method:
|
|
1669
|
+
* 1. Finds entries in our local store that the remote doesn't have
|
|
1670
|
+
* 2. Retrieves those entries from our local store
|
|
1671
|
+
* 3. Stores them in the remote store
|
|
1672
|
+
*
|
|
1673
|
+
* @param remoteStore The remote store to push entries to
|
|
1674
|
+
* @return A promise that resolves when the push is complete
|
|
1675
|
+
*/
|
|
1676
|
+
async pushChangesTo(remoteStore) {
|
|
1677
|
+
if (this.store.getId() !== remoteStore.getId()) {
|
|
1678
|
+
throw new Error(`[BaseMindooDB] Cannot push entries to the incompatible store ${this.store.getId()}`);
|
|
1679
|
+
}
|
|
1680
|
+
console.log(`[BaseMindooDB] Pushing entries to remote store ${remoteStore.getId()}`);
|
|
1681
|
+
// Get all entry IDs the remote store already has
|
|
1682
|
+
const remoteEntryIds = await remoteStore.getAllIds();
|
|
1683
|
+
console.log(`[BaseMindooDB] Remote store has ${remoteEntryIds.length} entry IDs`);
|
|
1684
|
+
// Find entries in our local store that the remote doesn't have
|
|
1685
|
+
const newEntryMetadata = await this.store.findNewEntries(remoteEntryIds);
|
|
1686
|
+
console.log(`[BaseMindooDB] Found ${newEntryMetadata.length} new entries to push`);
|
|
1687
|
+
if (newEntryMetadata.length === 0) {
|
|
1688
|
+
console.log(`[BaseMindooDB] No new entries to push`);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
// Get the full entries from our local store
|
|
1692
|
+
const newEntries = await this.store.getEntries(newEntryMetadata.map(em => em.id));
|
|
1693
|
+
console.log(`[BaseMindooDB] Retrieved ${newEntries.length} entries from local store`);
|
|
1694
|
+
// Store all entries in the remote store
|
|
1695
|
+
// The putEntries method handles deduplication (no-op if entry already exists)
|
|
1696
|
+
await remoteStore.putEntries(newEntries);
|
|
1697
|
+
console.log(`[BaseMindooDB] Pushed ${newEntries.length} entries to remote store`);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
exports.BaseMindooDB = BaseMindooDB;
|
|
1701
|
+
//# sourceMappingURL=BaseMindooDB.js.map
|