web3ql-client 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +66 -0
  2. package/contracts/PublicKeyRegistry.sol +87 -0
  3. package/dist/src/access.d.ts +176 -0
  4. package/dist/src/access.d.ts.map +1 -0
  5. package/dist/src/access.js +283 -0
  6. package/dist/src/access.js.map +1 -0
  7. package/dist/src/batch.d.ts +107 -0
  8. package/dist/src/batch.d.ts.map +1 -0
  9. package/dist/src/batch.js +188 -0
  10. package/dist/src/batch.js.map +1 -0
  11. package/dist/src/cli.d.ts +40 -0
  12. package/dist/src/cli.d.ts.map +1 -0
  13. package/dist/src/cli.js +361 -0
  14. package/dist/src/cli.js.map +1 -0
  15. package/dist/src/constraints.d.ts +126 -0
  16. package/dist/src/constraints.d.ts.map +1 -0
  17. package/dist/src/constraints.js +192 -0
  18. package/dist/src/constraints.js.map +1 -0
  19. package/dist/src/crypto.d.ts +118 -0
  20. package/dist/src/crypto.d.ts.map +1 -0
  21. package/dist/src/crypto.js +192 -0
  22. package/dist/src/crypto.js.map +1 -0
  23. package/dist/src/factory-client.d.ts +106 -0
  24. package/dist/src/factory-client.d.ts.map +1 -0
  25. package/dist/src/factory-client.js +202 -0
  26. package/dist/src/factory-client.js.map +1 -0
  27. package/dist/src/index-cache.d.ts +156 -0
  28. package/dist/src/index-cache.d.ts.map +1 -0
  29. package/dist/src/index-cache.js +265 -0
  30. package/dist/src/index-cache.js.map +1 -0
  31. package/dist/src/index.d.ts +60 -0
  32. package/dist/src/index.d.ts.map +1 -0
  33. package/dist/src/index.js +60 -0
  34. package/dist/src/index.js.map +1 -0
  35. package/dist/src/migrations.d.ts +114 -0
  36. package/dist/src/migrations.d.ts.map +1 -0
  37. package/dist/src/migrations.js +173 -0
  38. package/dist/src/migrations.js.map +1 -0
  39. package/dist/src/model.d.ts +198 -0
  40. package/dist/src/model.d.ts.map +1 -0
  41. package/dist/src/model.js +379 -0
  42. package/dist/src/model.js.map +1 -0
  43. package/dist/src/query.d.ts +155 -0
  44. package/dist/src/query.d.ts.map +1 -0
  45. package/dist/src/query.js +386 -0
  46. package/dist/src/query.js.map +1 -0
  47. package/dist/src/registry.d.ts +45 -0
  48. package/dist/src/registry.d.ts.map +1 -0
  49. package/dist/src/registry.js +80 -0
  50. package/dist/src/registry.js.map +1 -0
  51. package/dist/src/schema-manager.d.ts +109 -0
  52. package/dist/src/schema-manager.d.ts.map +1 -0
  53. package/dist/src/schema-manager.js +259 -0
  54. package/dist/src/schema-manager.js.map +1 -0
  55. package/dist/src/table-client.d.ts +156 -0
  56. package/dist/src/table-client.d.ts.map +1 -0
  57. package/dist/src/table-client.js +292 -0
  58. package/dist/src/table-client.js.map +1 -0
  59. package/dist/src/typed-table.d.ts +159 -0
  60. package/dist/src/typed-table.d.ts.map +1 -0
  61. package/dist/src/typed-table.js +246 -0
  62. package/dist/src/typed-table.js.map +1 -0
  63. package/dist/src/types.d.ts +48 -0
  64. package/dist/src/types.d.ts.map +1 -0
  65. package/dist/src/types.js +222 -0
  66. package/dist/src/types.js.map +1 -0
  67. package/keyManager.js +337 -0
  68. package/package.json +38 -0
  69. package/src/access.ts +421 -0
  70. package/src/batch.ts +259 -0
  71. package/src/cli.ts +349 -0
  72. package/src/constraints.ts +283 -0
  73. package/src/crypto.ts +239 -0
  74. package/src/factory-client.ts +237 -0
  75. package/src/index-cache.ts +351 -0
  76. package/src/index.ts +171 -0
  77. package/src/migrations.ts +215 -0
  78. package/src/model.ts +538 -0
  79. package/src/query.ts +508 -0
  80. package/src/registry.ts +100 -0
  81. package/src/schema-manager.ts +301 -0
  82. package/src/table-client.ts +393 -0
  83. package/src/typed-table.ts +340 -0
  84. package/src/types.ts +284 -0
  85. package/tsconfig.json +22 -0
  86. package/walletUtils.js +204 -0
package/src/cli.ts ADDED
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file cli.ts
4
+ * @notice Web3QL CLI — manage databases, tables, and records from the terminal.
5
+ *
6
+ * Install globally:
7
+ * npm install -g @web3ql/sdk
8
+ * web3ql --help
9
+ *
10
+ * Or run via npx:
11
+ * npx @web3ql/sdk <command>
12
+ *
13
+ * Requires a .env file (or env vars):
14
+ * PRIVATE_KEY=0x...
15
+ * RPC_URL=https://...
16
+ * FACTORY_ADDRESS=0x...
17
+ * REGISTRY_ADDRESS=0x...
18
+ *
19
+ * ─────────────────────────────────────────────────────────────
20
+ * Commands
21
+ * ─────────────────────────────────────────────────────────────
22
+ * web3ql db list — list your databases
23
+ * web3ql db create <name> — create a new database
24
+ *
25
+ * web3ql table list <dbAddress> — list tables in a database
26
+ * web3ql table create <dbAddress> "<SQL>" — create table from SQL
27
+ * web3ql table schema <tableAddress> — print schema for a table
28
+ *
29
+ * web3ql record write <tableAddress> <id> '<json>' — write a record
30
+ * web3ql record read <tableAddress> <id> — read + decrypt a record
31
+ * web3ql record list <tableAddress> — list your records
32
+ * web3ql record delete <tableAddress> <id> — delete a record
33
+ *
34
+ * web3ql query <tableAddress> --where "age > 18" --order "name asc" --limit 10
35
+ *
36
+ * web3ql info — show connected wallet + factory
37
+ * ─────────────────────────────────────────────────────────────
38
+ */
39
+
40
+ import { ethers } from 'ethers';
41
+ import { Web3QLClient } from './factory-client.js';
42
+ import { deriveKeypairFromWallet } from './crypto.js';
43
+
44
+ // ─────────────────────────────────────────────────────────────
45
+ // Env + provider setup
46
+ // ─────────────────────────────────────────────────────────────
47
+
48
+ function requireEnv(name: string): string {
49
+ const v = process.env[name];
50
+ if (!v) {
51
+ console.error(`\x1b[31mError:\x1b[0m Missing environment variable: ${name}`);
52
+ process.exit(1);
53
+ }
54
+ return v;
55
+ }
56
+
57
+ async function setup() {
58
+ // Support .env file via dotenvx if available
59
+ try {
60
+ const { config } = await import('@dotenvx/dotenvx');
61
+ config({ quiet: true });
62
+ } catch { /* no dotenvx installed — rely on process.env */ }
63
+
64
+ const privateKey = requireEnv('PRIVATE_KEY');
65
+ const rpcUrl = requireEnv('RPC_URL');
66
+ const factory = requireEnv('FACTORY_ADDRESS');
67
+ const registry = process.env['REGISTRY_ADDRESS'] ?? '';
68
+
69
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
70
+ const signer = new ethers.Wallet(privateKey, provider);
71
+ const keypair = await deriveKeypairFromWallet(signer);
72
+ const client = new Web3QLClient(factory, signer, keypair, registry);
73
+ const address = await signer.getAddress();
74
+
75
+ return { client, signer, keypair, address, factory, registry };
76
+ }
77
+
78
+ // ─────────────────────────────────────────────────────────────
79
+ // Formatting helpers
80
+ // ─────────────────────────────────────────────────────────────
81
+
82
+ const C = {
83
+ reset: '\x1b[0m',
84
+ bold: '\x1b[1m',
85
+ dim: '\x1b[2m',
86
+ green: '\x1b[32m',
87
+ cyan: '\x1b[36m',
88
+ yellow: '\x1b[33m',
89
+ red: '\x1b[31m',
90
+ };
91
+
92
+ function head(text: string) {
93
+ console.log(`\n${C.bold}${C.cyan}${text}${C.reset}`);
94
+ }
95
+ function ok(text: string) { console.log(`${C.green}✔${C.reset} ${text}`); }
96
+ function info(text: string) { console.log(`${C.dim}${text}${C.reset}`); }
97
+ function row(label: string, value: string) {
98
+ console.log(` ${C.bold}${label.padEnd(16)}${C.reset} ${value}`);
99
+ }
100
+
101
+ function shortAddr(addr: string) {
102
+ return `${addr.slice(0, 6)}…${addr.slice(-4)}`;
103
+ }
104
+
105
+ // ─────────────────────────────────────────────────────────────
106
+ // Command handlers
107
+ // ─────────────────────────────────────────────────────────────
108
+
109
+ async function cmdInfo() {
110
+ const { address, factory, registry } = await setup();
111
+ head('Web3QL — connection info');
112
+ row('Wallet', address);
113
+ row('Factory', factory);
114
+ row('Registry', registry ?? '(not set)');
115
+ console.log();
116
+ }
117
+
118
+ async function cmdDbList() {
119
+ const { client, address } = await setup();
120
+ head('Your databases');
121
+ const dbs = await client.getDatabases(address);
122
+ if (!dbs.length) { info('No databases found.'); return; }
123
+ dbs.forEach((addr: string, i: number) => {
124
+ console.log(` ${C.cyan}${i + 1}.${C.reset} ${addr}`);
125
+ });
126
+ console.log();
127
+ }
128
+
129
+ async function cmdDbCreate(name: string) {
130
+ const { client } = await setup();
131
+ head(`Creating database: ${name}`);
132
+ const db = await client.createDatabase(name);
133
+ ok(`Deployed! Address: ${db.address}`);
134
+ }
135
+
136
+ async function cmdTableList(dbAddress: string) {
137
+ const { client } = await setup();
138
+ head(`Tables in ${shortAddr(dbAddress)}`);
139
+ const db = client.database(dbAddress);
140
+ const tables: string[] = await db.listTables();
141
+ if (!tables.length) { info('No tables.'); return; }
142
+ tables.forEach((t: string, i: number) => {
143
+ console.log(` ${C.cyan}${i + 1}.${C.reset} ${t}`);
144
+ });
145
+ console.log();
146
+ }
147
+
148
+ async function cmdTableCreate(dbAddress: string, sql: string) {
149
+ const { client } = await setup();
150
+ head('Creating table');
151
+ info(sql);
152
+ // For CLI, pass empty schemaBytes — user should compile schema separately
153
+ const db = client.database(dbAddress);
154
+ // Extract table name from SQL for the name param
155
+ const nameMatch = sql.match(/CREATE\s+TABLE\s+([a-zA-Z_][a-zA-Z0-9_]*)/i);
156
+ const tableName = nameMatch?.[1] ?? 'table';
157
+ const addr = await db.createTable(tableName, '0x');
158
+ ok(`Table created! Address: ${addr}`);
159
+ }
160
+
161
+ async function cmdTableSchema(tableAddress: string) {
162
+ const { client } = await setup();
163
+ const { signer, keypair } = await setup();
164
+ const { EncryptedTableClient } = await import('./table-client.js');
165
+ const table = new EncryptedTableClient(tableAddress, signer, keypair);
166
+ head(`Schema — ${shortAddr(tableAddress)}`);
167
+ try {
168
+ const schemaHex: string = await (table as unknown as { getSchema(): Promise<string> }).getSchema();
169
+ if (!schemaHex || schemaHex === '0x') { info('No schema found.'); return; }
170
+ const bytes = Buffer.from(schemaHex.slice(2), 'hex');
171
+ console.log(bytes.toString('utf8'));
172
+ } catch {
173
+ info('Schema not available (table may use raw bytes storage).');
174
+ }
175
+ }
176
+
177
+ async function cmdRecordWrite(tableAddress: string, id: bigint, jsonStr: string) {
178
+ const { signer, keypair } = await setup();
179
+ const { EncryptedTableClient } = await import('./table-client.js');
180
+ const table = new EncryptedTableClient(tableAddress, signer, keypair);
181
+ const data = JSON.parse(jsonStr) as Record<string, unknown>;
182
+ head(`Writing record ${id} → ${shortAddr(tableAddress)}`);
183
+ const key = table.deriveKey(tableAddress, id);
184
+ const receipt = await table.writeRaw(key, JSON.stringify(data));
185
+ ok(`Written! Tx: ${receipt.hash}`);
186
+ }
187
+
188
+ async function cmdRecordRead(tableAddress: string, id: bigint) {
189
+ const { signer, keypair } = await setup();
190
+ const { EncryptedTableClient } = await import('./table-client.js');
191
+ const table = new EncryptedTableClient(tableAddress, signer, keypair);
192
+ head(`Record ${id} — ${shortAddr(tableAddress)}`);
193
+ try {
194
+ const key = table.deriveKey(tableAddress, id);
195
+ const plaintext = await table.readPlaintext(key);
196
+ console.log(JSON.stringify(JSON.parse(plaintext), null, 2));
197
+ } catch (e) {
198
+ console.error(`${C.red}Error:${C.reset}`, (e as Error).message);
199
+ }
200
+ }
201
+
202
+ async function cmdRecordList(tableAddress: string, limitN = 20) {
203
+ const { signer, keypair, address } = await setup();
204
+ const { EncryptedTableClient } = await import('./table-client.js');
205
+ const table = new EncryptedTableClient(tableAddress, signer, keypair);
206
+ head(`Records owned by ${shortAddr(address)} in ${shortAddr(tableAddress)}`);
207
+ const keys: string[] = await table.listOwnerRecords(address, 0n, BigInt(limitN));
208
+ if (!keys.length) { info('No records.'); return; }
209
+ for (const key of keys) {
210
+ try {
211
+ const plain: string = await table.readPlaintext(key);
212
+ const parsed = JSON.parse(plain) as unknown;
213
+ console.log(`\n ${C.cyan}Key:${C.reset} ${key}`);
214
+ console.log(' ' + JSON.stringify(parsed));
215
+ } catch { /* skip unreadable */ }
216
+ }
217
+ console.log();
218
+ }
219
+
220
+ async function cmdRecordDelete(tableAddress: string, id: bigint) {
221
+ const { signer, keypair } = await setup();
222
+ const { EncryptedTableClient } = await import('./table-client.js');
223
+ const table = new EncryptedTableClient(tableAddress, signer, keypair);
224
+ head(`Deleting record ${id}`);
225
+ const key = table.deriveKey(tableAddress, id);
226
+ const receipt = await table.deleteRecord(key);
227
+ ok(`Deleted! Tx: ${receipt.hash}`);
228
+ }
229
+
230
+ // ─────────────────────────────────────────────────────────────
231
+ // Help
232
+ // ─────────────────────────────────────────────────────────────
233
+
234
+ function printHelp() {
235
+ console.log(`
236
+ ${C.bold}${C.cyan}Web3QL CLI${C.reset} — on-chain encrypted database management
237
+
238
+ ${C.bold}USAGE${C.reset}
239
+ web3ql <command> [options]
240
+
241
+ ${C.bold}COMMANDS${C.reset}
242
+ ${C.green}info${C.reset} Show wallet + factory info
243
+ ${C.green}db list${C.reset} List your databases
244
+ ${C.green}db create <name>${C.reset} Deploy a new database
245
+ ${C.green}table list <dbAddress>${C.reset} List tables in a database
246
+ ${C.green}table create <dbAddress> "<SQL>"${C.reset} Create a table from SQL
247
+ ${C.green}table schema <tableAddress>${C.reset} Print table schema
248
+ ${C.green}record write <table> <id> '<json>'${C.reset} Write an encrypted record
249
+ ${C.green}record read <table> <id>${C.reset} Read + decrypt a record
250
+ ${C.green}record list <table> [limit]${C.reset} List your records
251
+ ${C.green}record delete <table> <id>${C.reset} Soft-delete a record
252
+
253
+ ${C.bold}ENV VARS${C.reset}
254
+ PRIVATE_KEY Ethereum private key (0x...)
255
+ RPC_URL JSON-RPC endpoint
256
+ FACTORY_ADDRESS Web3QL factory contract address
257
+ REGISTRY_ADDRESS Public key registry address (optional)
258
+
259
+ ${C.bold}EXAMPLE${C.reset}
260
+ export PRIVATE_KEY=0xabc...
261
+ export RPC_URL=https://forno.celo.org
262
+ export FACTORY_ADDRESS=0x2cfE...
263
+ web3ql info
264
+ web3ql db create myapp
265
+ web3ql table list 0xDB_ADDR
266
+ `);
267
+ }
268
+
269
+ // ─────────────────────────────────────────────────────────────
270
+ // Argument router
271
+ // ─────────────────────────────────────────────────────────────
272
+
273
+ async function main() {
274
+ const args = process.argv.slice(2);
275
+ const cmd = args[0];
276
+
277
+ try {
278
+ if (!cmd || cmd === '--help' || cmd === '-h') {
279
+ printHelp();
280
+ return;
281
+ }
282
+
283
+ if (cmd === 'info') {
284
+ await cmdInfo();
285
+ return;
286
+ }
287
+
288
+ if (cmd === 'db') {
289
+ const sub = args[1];
290
+ if (sub === 'list') { await cmdDbList(); return; }
291
+ if (sub === 'create') {
292
+ if (!args[2]) { console.error('Usage: web3ql db create <name>'); process.exit(1); }
293
+ await cmdDbCreate(args[2]!);
294
+ return;
295
+ }
296
+ }
297
+
298
+ if (cmd === 'table') {
299
+ const sub = args[1];
300
+ if (sub === 'list') {
301
+ if (!args[2]) { console.error('Usage: web3ql table list <dbAddress>'); process.exit(1); }
302
+ await cmdTableList(args[2]!);
303
+ return;
304
+ }
305
+ if (sub === 'create') {
306
+ if (!args[2] || !args[3]) { console.error('Usage: web3ql table create <dbAddress> "<SQL>"'); process.exit(1); }
307
+ await cmdTableCreate(args[2]!, args[3]!);
308
+ return;
309
+ }
310
+ if (sub === 'schema') {
311
+ if (!args[2]) { console.error('Usage: web3ql table schema <tableAddress>'); process.exit(1); }
312
+ await cmdTableSchema(args[2]!);
313
+ return;
314
+ }
315
+ }
316
+
317
+ if (cmd === 'record') {
318
+ const sub = args[1];
319
+ if (sub === 'write') {
320
+ if (!args[2] || !args[3] || !args[4]) { console.error('Usage: web3ql record write <table> <id> \'<json>\''); process.exit(1); }
321
+ await cmdRecordWrite(args[2]!, BigInt(args[3]!), args[4]!);
322
+ return;
323
+ }
324
+ if (sub === 'read') {
325
+ if (!args[2] || !args[3]) { console.error('Usage: web3ql record read <table> <id>'); process.exit(1); }
326
+ await cmdRecordRead(args[2]!, BigInt(args[3]!));
327
+ return;
328
+ }
329
+ if (sub === 'list') {
330
+ if (!args[2]) { console.error('Usage: web3ql record list <table> [limit]'); process.exit(1); }
331
+ await cmdRecordList(args[2]!, args[3] ? parseInt(args[3]) : 20);
332
+ return;
333
+ }
334
+ if (sub === 'delete') {
335
+ if (!args[2] || !args[3]) { console.error('Usage: web3ql record delete <table> <id>'); process.exit(1); }
336
+ await cmdRecordDelete(args[2]!, BigInt(args[3]!));
337
+ return;
338
+ }
339
+ }
340
+
341
+ console.error(`${C.red}Unknown command:${C.reset} ${cmd}\nRun \`web3ql --help\` for usage.`);
342
+ process.exit(1);
343
+ } catch (err) {
344
+ console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
345
+ process.exit(1);
346
+ }
347
+ }
348
+
349
+ main();
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @file constraints.ts
3
+ * @notice Web3QL v1.2 — integrity constraint engine.
4
+ *
5
+ * Constraints live in the SDK layer (off-chain) with the following guarantees:
6
+ *
7
+ * • PRIMARY KEY uniqueness — contract enforces at write (requires exists() pre-check)
8
+ * • UNIQUE — SDK maintains an in-memory seen-values set per column
9
+ * • DEFAULT / NOT NULL — enforced by types.ts validateAndEncode()
10
+ * • CHECK — per-column validation function
11
+ * • FOREIGN KEY — SDK reads target table at write time
12
+ * • AUTO_INCREMENT — SDK-maintained counter (persisted in a meta record)
13
+ * • ON DELETE CASCADE — SDK fetches referencing records and deletes them
14
+ *
15
+ * None of these require contract changes for CHECK/UNIQUE (client-side enforcement).
16
+ * PK uniqueness IS enforced at the contract level via `recordExists`.
17
+ *
18
+ * Usage:
19
+ * ─────────────────────────────────────────────────────────────
20
+ * const constraints = new ConstraintEngine([
21
+ * { type: 'unique', column: 'email' },
22
+ * { type: 'check', column: 'age', check: (v) => Number(v) >= 0 },
23
+ * { type: 'fk', column: 'userId', references: { table: userTable, column: 'id' } },
24
+ * ]);
25
+ *
26
+ * // Before writing:
27
+ * await constraints.validate(row, existingRows);
28
+ *
29
+ * // Get next AUTO_INCREMENT id:
30
+ * const nextId = await constraints.nextId(tableAddress, client);
31
+ * ─────────────────────────────────────────────────────────────
32
+ */
33
+
34
+ import type { EncryptedTableClient } from './table-client.js';
35
+
36
+ // ─────────────────────────────────────────────────────────────
37
+ // Constraint definition types
38
+ // ─────────────────────────────────────────────────────────────
39
+
40
+ export interface UniqueConstraint {
41
+ type : 'unique';
42
+ column: string;
43
+ }
44
+
45
+ export interface CheckConstraint {
46
+ type : 'check';
47
+ column: string;
48
+ /** Return true if the value is valid. Throw or return false to reject. */
49
+ check : (value: unknown, row: Record<string, unknown>) => boolean | Promise<boolean>;
50
+ /** Optional human-readable constraint name for error messages. */
51
+ name? : string;
52
+ }
53
+
54
+ export interface ForeignKeyConstraint {
55
+ type : 'fk';
56
+ column : string;
57
+ references: {
58
+ /** EncryptedTableClient for the target table. */
59
+ table : EncryptedTableClient;
60
+ /** Column in the target table that holds the referenced key. */
61
+ column: string;
62
+ /** Table name — needed to derive the bytes32 key. */
63
+ tableName: string;
64
+ };
65
+ /** What to do when the referenced record is deleted. Default: 'restrict' */
66
+ onDelete?: 'restrict' | 'cascade' | 'setNull';
67
+ }
68
+
69
+ export interface NotNullConstraint {
70
+ type : 'notNull';
71
+ column: string;
72
+ }
73
+
74
+ export type Constraint =
75
+ | UniqueConstraint
76
+ | CheckConstraint
77
+ | ForeignKeyConstraint
78
+ | NotNullConstraint;
79
+
80
+ // ─────────────────────────────────────────────────────────────
81
+ // Constraint violation error
82
+ // ─────────────────────────────────────────────────────────────
83
+
84
+ export class ConstraintViolation extends Error {
85
+ constructor(
86
+ public readonly constraintType: string,
87
+ public readonly column : string,
88
+ message: string,
89
+ ) {
90
+ super(message);
91
+ this.name = 'ConstraintViolation';
92
+ }
93
+ }
94
+
95
+ // ─────────────────────────────────────────────────────────────
96
+ // ConstraintEngine
97
+ // ─────────────────────────────────────────────────────────────
98
+
99
+ export class ConstraintEngine {
100
+ private constraints: Constraint[];
101
+
102
+ constructor(constraints: Constraint[] = []) {
103
+ this.constraints = constraints;
104
+ }
105
+
106
+ /**
107
+ * Validate a new/updated row against all constraints.
108
+ *
109
+ * @param row The row being written (fully encoded, post validateAndEncode).
110
+ * @param existingRows Already-decoded rows from the same table (for UNIQUE checks).
111
+ * Pass an empty array if fetching is not possible.
112
+ */
113
+ async validate(
114
+ row : Record<string, unknown>,
115
+ existingRows : Record<string, unknown>[] = [],
116
+ ): Promise<void> {
117
+ for (const c of this.constraints) {
118
+ switch (c.type) {
119
+ case 'notNull':
120
+ this._checkNotNull(c, row);
121
+ break;
122
+ case 'unique':
123
+ this._checkUnique(c, row, existingRows);
124
+ break;
125
+ case 'check':
126
+ await this._checkCheck(c, row);
127
+ break;
128
+ case 'fk':
129
+ await this._checkForeignKey(c, row);
130
+ break;
131
+ }
132
+ }
133
+ }
134
+
135
+ private _checkNotNull(c: NotNullConstraint, row: Record<string, unknown>): void {
136
+ const v = row[c.column];
137
+ if (v === null || v === undefined || v === '__NULL__') {
138
+ throw new ConstraintViolation(
139
+ 'notNull', c.column,
140
+ `NOT NULL violation: column "${c.column}" cannot be null`,
141
+ );
142
+ }
143
+ }
144
+
145
+ private _checkUnique(
146
+ c : UniqueConstraint,
147
+ row : Record<string, unknown>,
148
+ existingRows: Record<string, unknown>[],
149
+ ): void {
150
+ const newVal = row[c.column];
151
+ const conflict = existingRows.find(
152
+ (existing) => existing[c.column] === newVal && newVal !== null && newVal !== undefined,
153
+ );
154
+ if (conflict) {
155
+ throw new ConstraintViolation(
156
+ 'unique', c.column,
157
+ `UNIQUE violation: column "${c.column}" already has value "${String(newVal)}"`,
158
+ );
159
+ }
160
+ }
161
+
162
+ private async _checkCheck(c: CheckConstraint, row: Record<string, unknown>): Promise<void> {
163
+ const value = row[c.column];
164
+ const valid = await c.check(value, row);
165
+ if (!valid) {
166
+ throw new ConstraintViolation(
167
+ 'check', c.column,
168
+ `CHECK violation: column "${c.column}"${c.name ? ` (${c.name})` : ''} rejected value "${String(value)}"`,
169
+ );
170
+ }
171
+ }
172
+
173
+ private async _checkForeignKey(
174
+ c : ForeignKeyConstraint,
175
+ row: Record<string, unknown>,
176
+ ): Promise<void> {
177
+ const refValue = row[c.column];
178
+ if (refValue === null || refValue === undefined || refValue === '__NULL__') return; // NULL FK is allowed
179
+
180
+ const { table, tableName } = c.references;
181
+ const refKey = table.deriveKey(tableName, BigInt(String(refValue)));
182
+ const exists = await table.exists(refKey);
183
+ if (!exists) {
184
+ throw new ConstraintViolation(
185
+ 'fk', c.column,
186
+ `FOREIGN KEY violation: column "${c.column}" references non-existent record "${String(refValue)}"`,
187
+ );
188
+ }
189
+ }
190
+
191
+ /**
192
+ * ON DELETE CASCADE — delete all records in this table that reference
193
+ * the deleted row in the foreign-key column.
194
+ *
195
+ * @param deletedValue The primary-key value of the deleted parent record.
196
+ * @param fkColumn The column in this table that holds the FK.
197
+ * @param ownedRecords All decoded records in this table (to find referencing rows).
198
+ * @param deleteRecord Callback to actually delete a record by its bytes32 key.
199
+ */
200
+ async onDeleteCascade(
201
+ deletedValue : unknown,
202
+ fkColumn : string,
203
+ ownedRecords : { key: string; data: Record<string, unknown> }[],
204
+ deleteRecord : (key: string) => Promise<unknown>,
205
+ ): Promise<void> {
206
+ const toDelete = ownedRecords.filter(
207
+ (r) => String(r.data[fkColumn]) === String(deletedValue),
208
+ );
209
+ for (const r of toDelete) {
210
+ await deleteRecord(r.key);
211
+ }
212
+ }
213
+ }
214
+
215
+ // ─────────────────────────────────────────────────────────────
216
+ // AUTO_INCREMENT counter
217
+ // ─────────────────────────────────────────────────────────────
218
+
219
+ /**
220
+ * Client-side AUTO_INCREMENT counter backed by a special meta record on-chain.
221
+ *
222
+ * The counter is stored as a JSON record encrypted for the table owner under
223
+ * the key `keccak256("__auto_increment__" + tableName)`.
224
+ *
225
+ * ⚠ This is NOT atomic across multiple concurrent writers.
226
+ * For single-writer tables (personal data), it is safe.
227
+ * For multi-writer tables, use a relay-maintained counter (v1.2).
228
+ */
229
+ export class AutoIncrementCounter {
230
+ private tableName: string;
231
+ private client : EncryptedTableClient;
232
+ private metaKey : string;
233
+
234
+ constructor(tableName: string, client: EncryptedTableClient) {
235
+ this.tableName = tableName;
236
+ this.client = client;
237
+ this.metaKey = client.deriveKey(`__auto_increment__${tableName}`, 0n);
238
+ }
239
+
240
+ /**
241
+ * Read the current counter value. Returns 0 if not yet initialised.
242
+ */
243
+ async current(): Promise<bigint> {
244
+ try {
245
+ const exists = await this.client.exists(this.metaKey);
246
+ if (!exists) return 0n;
247
+ const json = await this.client.readPlaintext(this.metaKey);
248
+ const { counter } = JSON.parse(json) as { counter: string };
249
+ return BigInt(counter);
250
+ } catch {
251
+ return 0n;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Atomically increment and return the next ID.
257
+ * Reads current → increments → writes new value → returns incremented.
258
+ */
259
+ async next(): Promise<bigint> {
260
+ const cur = await this.current();
261
+ const nextId = cur + 1n;
262
+ const payload = JSON.stringify({ counter: nextId.toString() });
263
+
264
+ if (cur === 0n) {
265
+ await this.client.writeRaw(this.metaKey, payload);
266
+ } else {
267
+ await this.client.updateRaw(this.metaKey, payload);
268
+ }
269
+
270
+ return nextId;
271
+ }
272
+
273
+ /** Reset the counter (use with caution — may cause PK collisions). */
274
+ async reset(to: bigint = 0n): Promise<void> {
275
+ const payload = JSON.stringify({ counter: to.toString() });
276
+ const cur = await this.current();
277
+ if (cur === 0n) {
278
+ await this.client.writeRaw(this.metaKey, payload);
279
+ } else {
280
+ await this.client.updateRaw(this.metaKey, payload);
281
+ }
282
+ }
283
+ }