@enbox/dwn-sql-store 0.0.1 → 0.0.3
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 +36 -53
- package/dist/esm/src/data-store-sql.js +5 -5
- package/dist/esm/src/data-store-sql.js.map +1 -1
- package/dist/esm/src/dialect/bun-sqlite-adapter.js +46 -0
- package/dist/esm/src/dialect/bun-sqlite-adapter.js.map +1 -0
- package/dist/esm/src/dialect/mysql-dialect.js +1 -1
- package/dist/esm/src/dialect/mysql-dialect.js.map +1 -1
- package/dist/esm/src/dialect/postgres-dialect.js +1 -1
- package/dist/esm/src/dialect/postgres-dialect.js.map +1 -1
- package/dist/esm/src/dialect/sqlite-dialect.js +1 -1
- package/dist/esm/src/dialect/sqlite-dialect.js.map +1 -1
- package/dist/esm/src/main.js +3 -1
- package/dist/esm/src/main.js.map +1 -1
- package/dist/esm/src/message-store-sql.js +54 -25
- package/dist/esm/src/message-store-sql.js.map +1 -1
- package/dist/esm/src/resumable-task-store-sql.js +5 -6
- package/dist/esm/src/resumable-task-store-sql.js.map +1 -1
- package/dist/esm/src/smt-store-sql.js +151 -0
- package/dist/esm/src/smt-store-sql.js.map +1 -0
- package/dist/esm/src/state-index-sql.js +234 -0
- package/dist/esm/src/state-index-sql.js.map +1 -0
- package/dist/esm/src/utils/filter.js +3 -3
- package/dist/esm/src/utils/filter.js.map +1 -1
- package/dist/esm/src/utils/sanitize.js +7 -8
- package/dist/esm/src/utils/sanitize.js.map +1 -1
- package/dist/esm/src/utils/tags.js +3 -6
- package/dist/esm/src/utils/tags.js.map +1 -1
- package/dist/esm/src/utils/transaction.js +3 -21
- package/dist/esm/src/utils/transaction.js.map +1 -1
- package/dist/types/src/data-store-sql.d.ts +3 -4
- package/dist/types/src/data-store-sql.d.ts.map +1 -1
- package/dist/types/src/dialect/bun-sqlite-adapter.d.ts +33 -0
- package/dist/types/src/dialect/bun-sqlite-adapter.d.ts.map +1 -0
- package/dist/types/src/dialect/dialect.d.ts +1 -2
- package/dist/types/src/dialect/dialect.d.ts.map +1 -1
- package/dist/types/src/dialect/mysql-dialect.d.ts +3 -2
- package/dist/types/src/dialect/mysql-dialect.d.ts.map +1 -1
- package/dist/types/src/dialect/postgres-dialect.d.ts +3 -2
- package/dist/types/src/dialect/postgres-dialect.d.ts.map +1 -1
- package/dist/types/src/dialect/sqlite-dialect.d.ts +3 -2
- package/dist/types/src/dialect/sqlite-dialect.d.ts.map +1 -1
- package/dist/types/src/main.d.ts +3 -1
- package/dist/types/src/main.d.ts.map +1 -1
- package/dist/types/src/message-store-sql.d.ts +4 -3
- package/dist/types/src/message-store-sql.d.ts.map +1 -1
- package/dist/types/src/resumable-task-store-sql.d.ts +2 -2
- package/dist/types/src/resumable-task-store-sql.d.ts.map +1 -1
- package/dist/types/src/smt-store-sql.d.ts +37 -0
- package/dist/types/src/smt-store-sql.d.ts.map +1 -0
- package/dist/types/src/state-index-sql.d.ts +44 -0
- package/dist/types/src/state-index-sql.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +24 -42
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/utils/filter.d.ts +3 -3
- package/dist/types/src/utils/filter.d.ts.map +1 -1
- package/dist/types/src/utils/sanitize.d.ts +2 -2
- package/dist/types/src/utils/sanitize.d.ts.map +1 -1
- package/dist/types/src/utils/tags.d.ts +3 -5
- package/dist/types/src/utils/tags.d.ts.map +1 -1
- package/dist/types/src/utils/transaction.d.ts +4 -4
- package/dist/types/src/utils/transaction.d.ts.map +1 -1
- package/package.json +19 -31
- package/src/data-store-sql.ts +11 -9
- package/src/dialect/bun-sqlite-adapter.ts +82 -0
- package/src/dialect/dialect.ts +4 -5
- package/src/dialect/mysql-dialect.ts +8 -6
- package/src/dialect/postgres-dialect.ts +11 -6
- package/src/dialect/sqlite-dialect.ts +11 -6
- package/src/main.ts +4 -2
- package/src/message-store-sql.ts +90 -45
- package/src/resumable-task-store-sql.ts +9 -7
- package/src/smt-store-sql.ts +206 -0
- package/src/state-index-sql.ts +283 -0
- package/src/types.ts +32 -47
- package/src/utils/filter.ts +8 -6
- package/src/utils/sanitize.ts +19 -20
- package/src/utils/tags.ts +6 -7
- package/src/utils/transaction.ts +7 -23
- package/dist/cjs/main.js +0 -3784
- package/dist/cjs/package.json +0 -1
- package/dist/esm/src/event-log-sql.js +0 -169
- package/dist/esm/src/event-log-sql.js.map +0 -1
- package/dist/types/src/event-log-sql.d.ts +0 -24
- package/dist/types/src/event-log-sql.d.ts.map +0 -1
- package/src/event-log-sql.ts +0 -227
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL-backed implementation of SMTNodeStore.
|
|
3
|
+
*
|
|
4
|
+
* Storage layout:
|
|
5
|
+
* - Nodes are stored in the `stateIndexNodes` table, keyed by hex-encoded hash
|
|
6
|
+
* - The root hash is stored in the `stateIndexRoots` table
|
|
7
|
+
* - Node values are serialized with Uint8Array fields encoded as hex strings
|
|
8
|
+
*
|
|
9
|
+
* Each SMTStoreSql instance represents a single tree (identified by a tenant + scope key).
|
|
10
|
+
* Multiple instances share the same underlying Kysely database connection.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { DwnDatabaseType } from './types.js';
|
|
14
|
+
import type { Kysely } from 'kysely';
|
|
15
|
+
|
|
16
|
+
import type { Hash, SMTInternalNode, SMTLeafNode, SMTNode, SMTNodeStore } from '@enbox/dwn-sdk-js';
|
|
17
|
+
|
|
18
|
+
import { hashToHex, hexToHash } from '@enbox/dwn-sdk-js';
|
|
19
|
+
|
|
20
|
+
export type SMTStoreSqlParams = {
|
|
21
|
+
/** The shared Kysely database instance. */
|
|
22
|
+
db: Kysely<DwnDatabaseType>;
|
|
23
|
+
/** The tenant DID that owns this tree. */
|
|
24
|
+
tenant: string;
|
|
25
|
+
/** The scope key for this tree (e.g. '' for global, protocol URI for protocol-scoped). */
|
|
26
|
+
scope: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class SMTStoreSql implements SMTNodeStore {
|
|
30
|
+
#db: Kysely<DwnDatabaseType>;
|
|
31
|
+
#tenant: string;
|
|
32
|
+
#scope: string;
|
|
33
|
+
|
|
34
|
+
constructor({ db, tenant, scope }: SMTStoreSqlParams) {
|
|
35
|
+
this.#db = db;
|
|
36
|
+
this.#tenant = tenant;
|
|
37
|
+
this.#scope = scope;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async open(): Promise<void> {
|
|
41
|
+
// No-op: the Kysely DB is already open and tables are created by StateIndexSql.
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async close(): Promise<void> {
|
|
45
|
+
// No-op: the Kysely DB lifecycle is managed by StateIndexSql.
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async clear(): Promise<void> {
|
|
49
|
+
await this.#db
|
|
50
|
+
.deleteFrom('stateIndexNodes')
|
|
51
|
+
.where('tenant', '=', this.#tenant)
|
|
52
|
+
.where('scope', '=', this.#scope)
|
|
53
|
+
.execute();
|
|
54
|
+
|
|
55
|
+
await this.#db
|
|
56
|
+
.deleteFrom('stateIndexRoots')
|
|
57
|
+
.where('tenant', '=', this.#tenant)
|
|
58
|
+
.where('scope', '=', this.#scope)
|
|
59
|
+
.execute();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getNode(hash: Hash): Promise<SMTNode | undefined> {
|
|
63
|
+
const hexKey = hashToHex(hash);
|
|
64
|
+
|
|
65
|
+
const result = await this.#db
|
|
66
|
+
.selectFrom('stateIndexNodes')
|
|
67
|
+
.selectAll()
|
|
68
|
+
.where('tenant', '=', this.#tenant)
|
|
69
|
+
.where('scope', '=', this.#scope)
|
|
70
|
+
.where('nodeHash', '=', hexKey)
|
|
71
|
+
.executeTakeFirst();
|
|
72
|
+
|
|
73
|
+
if (!result) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return this.deserializeNode(result);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async putNode(hash: Hash, node: SMTNode): Promise<void> {
|
|
81
|
+
const hexKey = hashToHex(hash);
|
|
82
|
+
const values = this.serializeNodeToRow(hexKey, node);
|
|
83
|
+
|
|
84
|
+
// Use INSERT ... ON CONFLICT REPLACE pattern.
|
|
85
|
+
// Since different SQL dialects handle upsert differently, we use
|
|
86
|
+
// delete-then-insert as a portable approach.
|
|
87
|
+
await this.#db
|
|
88
|
+
.deleteFrom('stateIndexNodes')
|
|
89
|
+
.where('tenant', '=', this.#tenant)
|
|
90
|
+
.where('scope', '=', this.#scope)
|
|
91
|
+
.where('nodeHash', '=', hexKey)
|
|
92
|
+
.execute();
|
|
93
|
+
|
|
94
|
+
await this.#db
|
|
95
|
+
.insertInto('stateIndexNodes')
|
|
96
|
+
.values(values)
|
|
97
|
+
.execute();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async deleteNode(hash: Hash): Promise<void> {
|
|
101
|
+
const hexKey = hashToHex(hash);
|
|
102
|
+
|
|
103
|
+
await this.#db
|
|
104
|
+
.deleteFrom('stateIndexNodes')
|
|
105
|
+
.where('tenant', '=', this.#tenant)
|
|
106
|
+
.where('scope', '=', this.#scope)
|
|
107
|
+
.where('nodeHash', '=', hexKey)
|
|
108
|
+
.execute();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getRoot(): Promise<Hash | undefined> {
|
|
112
|
+
const result = await this.#db
|
|
113
|
+
.selectFrom('stateIndexRoots')
|
|
114
|
+
.select('rootHash')
|
|
115
|
+
.where('tenant', '=', this.#tenant)
|
|
116
|
+
.where('scope', '=', this.#scope)
|
|
117
|
+
.executeTakeFirst();
|
|
118
|
+
|
|
119
|
+
if (!result) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return hexToHash(result.rootHash);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async setRoot(hash: Hash): Promise<void> {
|
|
127
|
+
const hexRoot = hashToHex(hash);
|
|
128
|
+
|
|
129
|
+
// Delete-then-insert as a portable upsert.
|
|
130
|
+
await this.#db
|
|
131
|
+
.deleteFrom('stateIndexRoots')
|
|
132
|
+
.where('tenant', '=', this.#tenant)
|
|
133
|
+
.where('scope', '=', this.#scope)
|
|
134
|
+
.execute();
|
|
135
|
+
|
|
136
|
+
await this.#db
|
|
137
|
+
.insertInto('stateIndexRoots')
|
|
138
|
+
.values({
|
|
139
|
+
tenant : this.#tenant,
|
|
140
|
+
scope : this.#scope,
|
|
141
|
+
rootHash : hexRoot,
|
|
142
|
+
})
|
|
143
|
+
.execute();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Serialization helpers ──────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
private serializeNodeToRow(hexKey: string, node: SMTNode): {
|
|
149
|
+
tenant: string;
|
|
150
|
+
scope: string;
|
|
151
|
+
nodeHash: string;
|
|
152
|
+
nodeType: string;
|
|
153
|
+
leftHash: string | null;
|
|
154
|
+
rightHash: string | null;
|
|
155
|
+
leafKeyHash: string | null;
|
|
156
|
+
leafValueCid: string | null;
|
|
157
|
+
} {
|
|
158
|
+
if (node.type === 'internal') {
|
|
159
|
+
return {
|
|
160
|
+
tenant : this.#tenant,
|
|
161
|
+
scope : this.#scope,
|
|
162
|
+
nodeHash : hexKey,
|
|
163
|
+
nodeType : 'internal',
|
|
164
|
+
leftHash : hashToHex(node.leftHash),
|
|
165
|
+
rightHash : hashToHex(node.rightHash),
|
|
166
|
+
leafKeyHash : null,
|
|
167
|
+
leafValueCid : null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
tenant : this.#tenant,
|
|
173
|
+
scope : this.#scope,
|
|
174
|
+
nodeHash : hexKey,
|
|
175
|
+
nodeType : 'leaf',
|
|
176
|
+
leftHash : null,
|
|
177
|
+
rightHash : null,
|
|
178
|
+
leafKeyHash : hashToHex(node.keyHash),
|
|
179
|
+
leafValueCid : node.valueCid,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private deserializeNode(row: {
|
|
184
|
+
nodeType: string;
|
|
185
|
+
leftHash: string | null;
|
|
186
|
+
rightHash: string | null;
|
|
187
|
+
leafKeyHash: string | null;
|
|
188
|
+
leafValueCid: string | null;
|
|
189
|
+
}): SMTNode {
|
|
190
|
+
if (row.nodeType === 'internal') {
|
|
191
|
+
const node: SMTInternalNode = {
|
|
192
|
+
type : 'internal',
|
|
193
|
+
leftHash : hexToHash(row.leftHash!),
|
|
194
|
+
rightHash : hexToHash(row.rightHash!),
|
|
195
|
+
};
|
|
196
|
+
return node;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const node: SMTLeafNode = {
|
|
200
|
+
type : 'leaf',
|
|
201
|
+
keyHash : hexToHash(row.leafKeyHash!),
|
|
202
|
+
valueCid : row.leafValueCid!,
|
|
203
|
+
};
|
|
204
|
+
return node;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL-backed implementation of the StateIndex interface.
|
|
3
|
+
*
|
|
4
|
+
* Manages per-tenant Sparse Merkle Trees (global + per-protocol sub-trees) backed by SQL tables.
|
|
5
|
+
*
|
|
6
|
+
* Tables:
|
|
7
|
+
* - `stateIndexNodes`: stores SMT nodes (internal + leaf), keyed by (tenant, scope, nodeHash)
|
|
8
|
+
* - `stateIndexRoots`: stores the current root hash per (tenant, scope)
|
|
9
|
+
* - `stateIndexMeta`: reverse lookup from messageCid → protocol (for deletion)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Dialect } from './dialect/dialect.js';
|
|
13
|
+
import type { DwnDatabaseType } from './types.js';
|
|
14
|
+
import type { Hash } from '@enbox/dwn-sdk-js';
|
|
15
|
+
import type { KeyValues } from '@enbox/dwn-sdk-js';
|
|
16
|
+
import type { StateIndex } from '@enbox/dwn-sdk-js';
|
|
17
|
+
|
|
18
|
+
import { initDefaultHashes } from '@enbox/dwn-sdk-js';
|
|
19
|
+
import { Kysely } from 'kysely';
|
|
20
|
+
import { SMTStoreSql } from './smt-store-sql.js';
|
|
21
|
+
import { SparseMerkleTree } from '@enbox/dwn-sdk-js';
|
|
22
|
+
|
|
23
|
+
export class StateIndexSql implements StateIndex {
|
|
24
|
+
#dialect: Dialect;
|
|
25
|
+
#db: Kysely<DwnDatabaseType> | null = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cache of per-tenant global SMTs. Lazily populated on first access.
|
|
29
|
+
* Stores promises to avoid race conditions when multiple concurrent operations
|
|
30
|
+
* trigger lazy initialization for the same tenant.
|
|
31
|
+
*/
|
|
32
|
+
#globalTrees: Map<string, Promise<SparseMerkleTree>> = new Map();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cache of per-tenant, per-protocol SMTs. Key format: `{tenant}\x00{protocol}`.
|
|
36
|
+
* Stores promises to avoid race conditions.
|
|
37
|
+
*/
|
|
38
|
+
#protocolTrees: Map<string, Promise<SparseMerkleTree>> = new Map();
|
|
39
|
+
|
|
40
|
+
constructor(dialect: Dialect) {
|
|
41
|
+
this.#dialect = dialect;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async open(): Promise<void> {
|
|
45
|
+
if (this.#db) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.#db = new Kysely<DwnDatabaseType>({ dialect: this.#dialect });
|
|
50
|
+
|
|
51
|
+
// Ensure default hashes are initialized for the SMT
|
|
52
|
+
await initDefaultHashes();
|
|
53
|
+
|
|
54
|
+
// ─── Create stateIndexNodes table ─────────────────────────────────────
|
|
55
|
+
const nodesTableName = 'stateIndexNodes';
|
|
56
|
+
const nodesTableExists = await this.#dialect.hasTable(this.#db, nodesTableName);
|
|
57
|
+
if (!nodesTableExists) {
|
|
58
|
+
await this.#db.schema
|
|
59
|
+
.createTable(nodesTableName)
|
|
60
|
+
.ifNotExists()
|
|
61
|
+
.addColumn('tenant', 'varchar(255)', (col) => col.notNull())
|
|
62
|
+
.addColumn('scope', 'varchar(200)', (col) => col.notNull())
|
|
63
|
+
.addColumn('nodeHash', 'varchar(64)', (col) => col.notNull())
|
|
64
|
+
.addColumn('nodeType', 'varchar(10)', (col) => col.notNull())
|
|
65
|
+
.addColumn('leftHash', 'varchar(64)')
|
|
66
|
+
.addColumn('rightHash', 'varchar(64)')
|
|
67
|
+
.addColumn('leafKeyHash', 'varchar(64)')
|
|
68
|
+
.addColumn('leafValueCid', 'varchar(60)')
|
|
69
|
+
.execute();
|
|
70
|
+
|
|
71
|
+
// Not UNIQUE because the delete-then-insert upsert pattern in SMTStoreSql
|
|
72
|
+
// can race under concurrent access, causing duplicate key violations.
|
|
73
|
+
await this.#db.schema
|
|
74
|
+
.createIndex('index_stateIndexNodes_tenant_scope_nodeHash')
|
|
75
|
+
.on(nodesTableName)
|
|
76
|
+
.columns(['tenant', 'scope', 'nodeHash'])
|
|
77
|
+
.execute();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Create stateIndexRoots table ─────────────────────────────────────
|
|
81
|
+
const rootsTableName = 'stateIndexRoots';
|
|
82
|
+
const rootsTableExists = await this.#dialect.hasTable(this.#db, rootsTableName);
|
|
83
|
+
if (!rootsTableExists) {
|
|
84
|
+
await this.#db.schema
|
|
85
|
+
.createTable(rootsTableName)
|
|
86
|
+
.ifNotExists()
|
|
87
|
+
.addColumn('tenant', 'varchar(255)', (col) => col.notNull())
|
|
88
|
+
.addColumn('scope', 'varchar(200)', (col) => col.notNull())
|
|
89
|
+
.addColumn('rootHash', 'varchar(64)', (col) => col.notNull())
|
|
90
|
+
.execute();
|
|
91
|
+
|
|
92
|
+
await this.#db.schema
|
|
93
|
+
.createIndex('index_stateIndexRoots_tenant_scope')
|
|
94
|
+
.on(rootsTableName)
|
|
95
|
+
.columns(['tenant', 'scope'])
|
|
96
|
+
.execute();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Create stateIndexMeta table ──────────────────────────────────────
|
|
100
|
+
const metaTableName = 'stateIndexMeta';
|
|
101
|
+
const metaTableExists = await this.#dialect.hasTable(this.#db, metaTableName);
|
|
102
|
+
if (!metaTableExists) {
|
|
103
|
+
await this.#db.schema
|
|
104
|
+
.createTable(metaTableName)
|
|
105
|
+
.ifNotExists()
|
|
106
|
+
.addColumn('tenant', 'varchar(255)', (col) => col.notNull())
|
|
107
|
+
.addColumn('messageCid', 'varchar(60)', (col) => col.notNull())
|
|
108
|
+
.addColumn('protocol', 'varchar(200)')
|
|
109
|
+
.execute();
|
|
110
|
+
|
|
111
|
+
await this.#db.schema
|
|
112
|
+
.createIndex('index_stateIndexMeta_tenant_messageCid')
|
|
113
|
+
.on(metaTableName)
|
|
114
|
+
.columns(['tenant', 'messageCid'])
|
|
115
|
+
.execute();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async close(): Promise<void> {
|
|
120
|
+
this.#globalTrees.clear();
|
|
121
|
+
this.#protocolTrees.clear();
|
|
122
|
+
await this.#db?.destroy();
|
|
123
|
+
this.#db = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async clear(): Promise<void> {
|
|
127
|
+
if (!this.#db) {
|
|
128
|
+
throw new Error('Connection to database not open. Call `open` before using `clear`.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.#globalTrees.clear();
|
|
132
|
+
this.#protocolTrees.clear();
|
|
133
|
+
|
|
134
|
+
await this.#db.deleteFrom('stateIndexNodes').execute();
|
|
135
|
+
await this.#db.deleteFrom('stateIndexRoots').execute();
|
|
136
|
+
await this.#db.deleteFrom('stateIndexMeta').execute();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async insert(tenant: string, messageCid: string, indexes: KeyValues): Promise<void> {
|
|
140
|
+
if (!this.#db) {
|
|
141
|
+
throw new Error('Connection to database not open. Call `open` before using `insert`.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Insert into the global tree
|
|
145
|
+
const globalSmt = await this.getGlobalTree(tenant);
|
|
146
|
+
await globalSmt.insert(messageCid);
|
|
147
|
+
|
|
148
|
+
// If the message is associated with a protocol, insert into the protocol-scoped tree
|
|
149
|
+
const protocol = indexes.protocol as string | undefined;
|
|
150
|
+
if (protocol !== undefined) {
|
|
151
|
+
const protoSmt = await this.getProtocolTree(tenant, protocol);
|
|
152
|
+
await protoSmt.insert(messageCid);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Store reverse lookup metadata for deletion
|
|
156
|
+
await this.#db
|
|
157
|
+
.insertInto('stateIndexMeta')
|
|
158
|
+
.values({
|
|
159
|
+
tenant,
|
|
160
|
+
messageCid,
|
|
161
|
+
protocol: protocol ?? null,
|
|
162
|
+
})
|
|
163
|
+
.execute();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async delete(tenant: string, messageCids: string[]): Promise<void> {
|
|
167
|
+
if (!this.#db) {
|
|
168
|
+
throw new Error('Connection to database not open. Call `open` before using `delete`.');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (messageCids.length === 0) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const globalSmt = await this.getGlobalTree(tenant);
|
|
176
|
+
|
|
177
|
+
for (const messageCid of messageCids) {
|
|
178
|
+
// Look up stored metadata to find the protocol
|
|
179
|
+
const meta = await this.#db
|
|
180
|
+
.selectFrom('stateIndexMeta')
|
|
181
|
+
.select('protocol')
|
|
182
|
+
.where('tenant', '=', tenant)
|
|
183
|
+
.where('messageCid', '=', messageCid)
|
|
184
|
+
.executeTakeFirst();
|
|
185
|
+
|
|
186
|
+
// Delete from global tree
|
|
187
|
+
await globalSmt.delete(messageCid);
|
|
188
|
+
|
|
189
|
+
// Delete from protocol tree if applicable
|
|
190
|
+
if (meta?.protocol) {
|
|
191
|
+
const protoSmt = await this.getProtocolTree(tenant, meta.protocol);
|
|
192
|
+
await protoSmt.delete(messageCid);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Remove the reverse lookup
|
|
196
|
+
await this.#db
|
|
197
|
+
.deleteFrom('stateIndexMeta')
|
|
198
|
+
.where('tenant', '=', tenant)
|
|
199
|
+
.where('messageCid', '=', messageCid)
|
|
200
|
+
.execute();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async getRoot(tenant: string): Promise<Hash> {
|
|
205
|
+
const smt = await this.getGlobalTree(tenant);
|
|
206
|
+
return smt.getRoot();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async getProtocolRoot(tenant: string, protocol: string): Promise<Hash> {
|
|
210
|
+
const smt = await this.getProtocolTree(tenant, protocol);
|
|
211
|
+
return smt.getRoot();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getSubtreeHash(tenant: string, prefix: boolean[]): Promise<Hash> {
|
|
215
|
+
const smt = await this.getGlobalTree(tenant);
|
|
216
|
+
return smt.getSubtreeHash(prefix);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async getProtocolSubtreeHash(tenant: string, protocol: string, prefix: boolean[]): Promise<Hash> {
|
|
220
|
+
const smt = await this.getProtocolTree(tenant, protocol);
|
|
221
|
+
return smt.getSubtreeHash(prefix);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async getLeaves(tenant: string, prefix: boolean[]): Promise<string[]> {
|
|
225
|
+
const smt = await this.getGlobalTree(tenant);
|
|
226
|
+
return smt.getLeaves(prefix);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async getProtocolLeaves(tenant: string, protocol: string, prefix: boolean[]): Promise<string[]> {
|
|
230
|
+
const smt = await this.getProtocolTree(tenant, protocol);
|
|
231
|
+
return smt.getLeaves(prefix);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─── Private helpers ────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get or create the global SMT for a tenant.
|
|
238
|
+
* Uses a promise-based cache to prevent concurrent callers from racing.
|
|
239
|
+
*/
|
|
240
|
+
private getGlobalTree(tenant: string): Promise<SparseMerkleTree> {
|
|
241
|
+
let smtPromise = this.#globalTrees.get(tenant);
|
|
242
|
+
if (smtPromise !== undefined) {
|
|
243
|
+
return smtPromise;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
smtPromise = this.createTree(tenant, '');
|
|
247
|
+
this.#globalTrees.set(tenant, smtPromise);
|
|
248
|
+
return smtPromise;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get or create a protocol-scoped SMT for a tenant.
|
|
253
|
+
* Uses a promise-based cache to prevent concurrent callers from racing.
|
|
254
|
+
*/
|
|
255
|
+
private getProtocolTree(tenant: string, protocol: string): Promise<SparseMerkleTree> {
|
|
256
|
+
const cacheKey = `${tenant}\x00${protocol}`;
|
|
257
|
+
let smtPromise = this.#protocolTrees.get(cacheKey);
|
|
258
|
+
if (smtPromise !== undefined) {
|
|
259
|
+
return smtPromise;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
smtPromise = this.createTree(tenant, protocol);
|
|
263
|
+
this.#protocolTrees.set(cacheKey, smtPromise);
|
|
264
|
+
return smtPromise;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create and initialize a new SparseMerkleTree backed by SQL via SMTStoreSql.
|
|
269
|
+
*/
|
|
270
|
+
private async createTree(tenant: string, scope: string): Promise<SparseMerkleTree> {
|
|
271
|
+
const store = new SMTStoreSql({
|
|
272
|
+
db: this.#db!,
|
|
273
|
+
tenant,
|
|
274
|
+
scope,
|
|
275
|
+
});
|
|
276
|
+
const smt = new SparseMerkleTree(store);
|
|
277
|
+
await smt.initialize();
|
|
278
|
+
return smt;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
package/src/types.ts
CHANGED
|
@@ -1,39 +1,7 @@
|
|
|
1
1
|
import type { Generated } from 'kysely';
|
|
2
|
+
import type { KeyValues } from '@enbox/dwn-sdk-js';
|
|
2
3
|
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
type EventLogTable = {
|
|
6
|
-
watermark: Generated<number>;
|
|
7
|
-
tenant: string;
|
|
8
|
-
messageCid: string;
|
|
9
|
-
|
|
10
|
-
// "indexes" start
|
|
11
|
-
interface: string | null;
|
|
12
|
-
method: string | null;
|
|
13
|
-
schema: string | null;
|
|
14
|
-
dataCid: string | null;
|
|
15
|
-
dataSize: number | null;
|
|
16
|
-
dateCreated: string | null;
|
|
17
|
-
messageTimestamp: string | null;
|
|
18
|
-
dataFormat: string | null;
|
|
19
|
-
isLatestBaseState: boolean | null;
|
|
20
|
-
published: boolean | null;
|
|
21
|
-
author: string | null;
|
|
22
|
-
recordId: string | null;
|
|
23
|
-
entryId: string | null;
|
|
24
|
-
datePublished: string | null;
|
|
25
|
-
latest: string | null;
|
|
26
|
-
protocol: string | null;
|
|
27
|
-
permissionsRequestId: string | null;
|
|
28
|
-
attester: string | null;
|
|
29
|
-
protocolPath: string | null;
|
|
30
|
-
recipient: string | null;
|
|
31
|
-
contextId: string | null;
|
|
32
|
-
parentId: string | null;
|
|
33
|
-
permissionGrantId: string | null;
|
|
34
|
-
prune: boolean | null;
|
|
35
|
-
// "indexes" end
|
|
36
|
-
}
|
|
4
|
+
export type { KeyValues };
|
|
37
5
|
|
|
38
6
|
type MessageStoreTable = {
|
|
39
7
|
id: Generated<number>;
|
|
@@ -57,7 +25,6 @@ type MessageStoreTable = {
|
|
|
57
25
|
entryId: string | null;
|
|
58
26
|
datePublished: string | null;
|
|
59
27
|
protocol: string | null;
|
|
60
|
-
permissionsRequestId: string | null;
|
|
61
28
|
attester: string | null;
|
|
62
29
|
protocolPath: string | null;
|
|
63
30
|
recipient: string | null;
|
|
@@ -66,7 +33,7 @@ type MessageStoreTable = {
|
|
|
66
33
|
permissionGrantId: string | null;
|
|
67
34
|
prune: boolean | null;
|
|
68
35
|
// "indexes" end
|
|
69
|
-
}
|
|
36
|
+
};
|
|
70
37
|
|
|
71
38
|
type MessageStoreRecordsTagsTable = {
|
|
72
39
|
id: Generated<number>;
|
|
@@ -76,12 +43,29 @@ type MessageStoreRecordsTagsTable = {
|
|
|
76
43
|
valueNumber: number | null;
|
|
77
44
|
};
|
|
78
45
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
46
|
+
// ─── StateIndex SMT tables ────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
type StateIndexNodeTable = {
|
|
49
|
+
tenant: string;
|
|
50
|
+
scope: string;
|
|
51
|
+
nodeHash: string;
|
|
52
|
+
nodeType: string;
|
|
53
|
+
leftHash: string | null;
|
|
54
|
+
rightHash: string | null;
|
|
55
|
+
leafKeyHash: string | null;
|
|
56
|
+
leafValueCid: string | null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type StateIndexRootTable = {
|
|
60
|
+
tenant: string;
|
|
61
|
+
scope: string;
|
|
62
|
+
rootHash: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type StateIndexMetaTable = {
|
|
66
|
+
tenant: string;
|
|
67
|
+
messageCid: string;
|
|
68
|
+
protocol: string | null;
|
|
85
69
|
};
|
|
86
70
|
|
|
87
71
|
type DataStoreTable = {
|
|
@@ -90,20 +74,21 @@ type DataStoreTable = {
|
|
|
90
74
|
recordId: string;
|
|
91
75
|
dataCid: string;
|
|
92
76
|
data: Uint8Array;
|
|
93
|
-
}
|
|
77
|
+
};
|
|
94
78
|
|
|
95
79
|
type ResumableTaskTable = {
|
|
96
80
|
id: string;
|
|
97
81
|
task: string;
|
|
98
82
|
timeout: number;
|
|
99
83
|
retryCount: number;
|
|
100
|
-
}
|
|
84
|
+
};
|
|
101
85
|
|
|
102
86
|
export type DwnDatabaseType = {
|
|
103
|
-
eventLogMessages: EventLogTable;
|
|
104
|
-
eventLogRecordsTags: EventLogRecordsTagsTable;
|
|
105
87
|
messageStoreMessages: MessageStoreTable;
|
|
106
88
|
messageStoreRecordsTags: MessageStoreRecordsTagsTable;
|
|
107
89
|
dataStore: DataStoreTable;
|
|
108
90
|
resumableTasks: ResumableTaskTable;
|
|
109
|
-
|
|
91
|
+
stateIndexNodes: StateIndexNodeTable;
|
|
92
|
+
stateIndexRoots: StateIndexRootTable;
|
|
93
|
+
stateIndexMeta: StateIndexMetaTable;
|
|
94
|
+
};
|
package/src/utils/filter.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import type { DwnDatabaseType } from '../types.js';
|
|
2
|
+
import type { Filter } from '@enbox/dwn-sdk-js';
|
|
3
|
+
import type { ExpressionBuilder, OperandExpression, SelectQueryBuilder, SqlBool } from 'kysely';
|
|
4
|
+
|
|
5
|
+
import { DynamicModule } from 'kysely';
|
|
6
|
+
import { sanitizedValue, sanitizeFiltersAndSeparateTags } from './sanitize.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Takes multiple Filters and returns a single query.
|
|
@@ -44,7 +46,7 @@ function processFilter<DB = DwnDatabaseType, TB extends keyof DB = keyof DB>(
|
|
|
44
46
|
andOperands: OperandExpression<SqlBool>[],
|
|
45
47
|
filter: Filter
|
|
46
48
|
): void {
|
|
47
|
-
for (
|
|
49
|
+
for (const property in filter) {
|
|
48
50
|
const value = filter[property];
|
|
49
51
|
const column = new DynamicModule().ref(property);
|
|
50
52
|
if (Array.isArray(value)) { // OneOfFilter
|
|
@@ -87,7 +89,7 @@ function processTags<DB = DwnDatabaseType, TB extends keyof DB = keyof DB>(
|
|
|
87
89
|
const valueString = new DynamicModule().ref('valueString');
|
|
88
90
|
|
|
89
91
|
// process each tag and add it to the andOperands from the rest of the filters
|
|
90
|
-
for (
|
|
92
|
+
for (const property in tags) {
|
|
91
93
|
andOperands.push(eb(tagColumn, '=', property));
|
|
92
94
|
const value = tags[property];
|
|
93
95
|
if (Array.isArray(value)) { // OneOfFilter
|