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
@@ -0,0 +1,340 @@
1
+ /**
2
+ * @file typed-table.ts
3
+ * @notice High-level Prisma-style API for Web3QL encrypted tables.
4
+ *
5
+ * v1.1 upgrades:
6
+ * • Optional SchemaDefinition support — auto validates, encodes, and decodes
7
+ * fields using the extended type system (TIMESTAMP, UUID, ENUM, DECIMAL, etc.)
8
+ * • NOT NULL + DEFAULT enforcement on write
9
+ * • findMany with full query builder support: where/orderBy/limit/select/distinct
10
+ * • findAll — convenience method (fetches + decrypts all records in batches)
11
+ * • aggregate — COUNT/SUM/AVG/MIN/MAX over filtered decrypted records
12
+ * • seed — bulk insert an array of records
13
+ *
14
+ * Usage (basic, schema-less — identical to v1.0):
15
+ * ─────────────────────────────────────────────────────────────
16
+ * const users = new TypedTableClient<User>('users', db.table('0xTABLE'))
17
+ * await users.create(1n, { id: 1n, name: 'Alice' })
18
+ * const alice = await users.findUnique(1n)
19
+ *
20
+ * Usage (with schema for validation + type coercion):
21
+ * ─────────────────────────────────────────────────────────────
22
+ * const schema: SchemaDefinition = [
23
+ * { name: 'id', type: 'INT', primaryKey: true },
24
+ * { name: 'name', type: 'TEXT', notNull: true },
25
+ * { name: 'email', type: 'TEXT', notNull: true },
26
+ * { name: 'createdAt', type: 'TIMESTAMP', default: () => new Date() },
27
+ * { name: 'role', type: 'ENUM', enumValues: ['user','admin'], default: 'user' },
28
+ * ]
29
+ * const users = new TypedTableClient<User>('users', db.table('0xADDR'), schema)
30
+ *
31
+ * // findMany with query builder
32
+ * const admins = await users.findMany(ownerAddr, {
33
+ * where: [['role', 'eq', 'admin']],
34
+ * orderBy: [['createdAt', 'desc']],
35
+ * limit: 20,
36
+ * select: ['id', 'name', 'email'],
37
+ * })
38
+ * ─────────────────────────────────────────────────────────────
39
+ */
40
+
41
+ import { ethers } from 'ethers';
42
+ import type { EncryptedTableClient } from './table-client.js';
43
+ import {
44
+ SchemaDefinition,
45
+ validateAndEncode,
46
+ decodeRow,
47
+ } from './types.js';
48
+ import {
49
+ query as buildQuery,
50
+ WhereOperator,
51
+ SortDirection,
52
+ AggregateOptions,
53
+ AggregateResult,
54
+ } from './query.js';
55
+
56
+ export type { SchemaDefinition } from './types.js';
57
+
58
+ // ─────────────────────────────────────────────────────────────
59
+ // Public API types
60
+ // ─────────────────────────────────────────────────────────────
61
+
62
+ /** A single WHERE clause expressed as a tuple. */
63
+ export type WhereTuple =
64
+ | [field: string, op: 'isNull' | 'isNotNull']
65
+ | [field: string, op: 'in' | 'notIn', values: unknown[]]
66
+ | [field: string, op: 'between', range: [unknown, unknown]]
67
+ | [field: string, op: WhereOperator, value: unknown];
68
+
69
+ export interface FindManyOptions {
70
+ /**
71
+ * Starting index into the owner's record list when fetching from chain.
72
+ * Applied BEFORE client-side filtering — increase if paginating large tables.
73
+ * Default: 0.
74
+ */
75
+ chainOffset?: bigint;
76
+ /**
77
+ * Maximum records to fetch from chain per page.
78
+ * Default: 200 (raised from v1.0's 50 to allow client-side filtering).
79
+ */
80
+ chainLimit?: bigint;
81
+ /** WHERE conditions — ANDed together. Applied after decrypt. */
82
+ where?: WhereTuple[];
83
+ /** Sort order — applied after filtering. */
84
+ orderBy?: [field: string, dir?: SortDirection][];
85
+ /** Max records returned after filtering (client-side LIMIT). */
86
+ limit?: number;
87
+ /** Skip N records after filtering (client-side OFFSET). */
88
+ offset?: number;
89
+ /** Return only these fields. */
90
+ select?: string[];
91
+ /** Deduplicate on this field value. */
92
+ distinct?: string;
93
+ }
94
+
95
+ export interface RecordWithId<T> {
96
+ /**
97
+ * The uint256 primary key cannot be recovered from the on-chain bytes32 key alone
98
+ * (it is a keccak256 hash). This field is always `0n` when records are fetched via
99
+ * `findMany()`. Store the primary key inside your data payload and read it from
100
+ * `record.data.id` instead.
101
+ */
102
+ id : bigint;
103
+ /** Decrypted, typed record data. */
104
+ data : T;
105
+ /** bytes32 on-chain key (hex string). */
106
+ recordKey: string;
107
+ }
108
+
109
+ // ─────────────────────────────────────────────────────────────
110
+ // TypedTableClient
111
+ // ─────────────────────────────────────────────────────────────
112
+
113
+ export class TypedTableClient<T extends Record<string, unknown>> {
114
+ private tableName : string;
115
+ private inner : EncryptedTableClient;
116
+ private schema? : SchemaDefinition;
117
+
118
+ /**
119
+ * @param tableName Must match the name used in the SQL schema and createTable().
120
+ * @param inner An EncryptedTableClient from DatabaseClient.table(address).
121
+ * @param schema Optional field descriptors — enables validation, type coercion,
122
+ * NOT NULL enforcement, and DEFAULT values.
123
+ */
124
+ constructor(tableName: string, inner: EncryptedTableClient, schema?: SchemaDefinition) {
125
+ this.tableName = tableName;
126
+ this.inner = inner;
127
+ this.schema = schema;
128
+ }
129
+
130
+ // ── Key helper ─────────────────────────────────────────────
131
+
132
+ /** Derive the canonical bytes32 on-chain key for a given primary key id. */
133
+ key(id: bigint): string {
134
+ return this.inner.deriveKey(this.tableName, id);
135
+ }
136
+
137
+ // ── Internal encode/decode ──────────────────────────────────
138
+
139
+ private encode(data: T): string {
140
+ if (this.schema) {
141
+ const wire = validateAndEncode(this.schema, data as Record<string, unknown>);
142
+ return JSON.stringify(wire);
143
+ }
144
+ return JSON.stringify(data);
145
+ }
146
+
147
+ private decode(plaintext: string): T {
148
+ const parsed = JSON.parse(plaintext) as Record<string, unknown>;
149
+ if (this.schema) return decodeRow(this.schema, parsed) as T;
150
+ return parsed as T;
151
+ }
152
+
153
+ // ── Write ───────────────────────────────────────────────────
154
+
155
+ /**
156
+ * Validate, encode, encrypt, and store a new record.
157
+ * Throws if a non-deleted record with the same id already exists.
158
+ */
159
+ async create(id: bigint, data: T): Promise<ethers.TransactionReceipt> {
160
+ return this.inner.writeRaw(this.key(id), this.encode(data));
161
+ }
162
+
163
+ /**
164
+ * Bulk-insert an array of records. Each record must include the primary key field.
165
+ * Records are written sequentially — fails on first error.
166
+ */
167
+ async seed(
168
+ rows: { id: bigint; data: T }[],
169
+ ): Promise<ethers.TransactionReceipt[]> {
170
+ const receipts: ethers.TransactionReceipt[] = [];
171
+ for (const row of rows) {
172
+ receipts.push(await this.create(row.id, row.data));
173
+ }
174
+ return receipts;
175
+ }
176
+
177
+ // ── Read ────────────────────────────────────────────────────
178
+
179
+ /**
180
+ * Read and decrypt a single record by primary key.
181
+ * Returns `null` if the record does not exist or has been deleted.
182
+ */
183
+ async findUnique(id: bigint): Promise<T | null> {
184
+ try {
185
+ const exists = await this.inner.exists(this.key(id));
186
+ if (!exists) return null;
187
+ const plaintext = await this.inner.readPlaintext(this.key(id));
188
+ return this.decode(plaintext);
189
+ } catch (err: unknown) {
190
+ const msg = (err as Error).message ?? '';
191
+ if (msg.includes('deleted') || msg.includes('not found') || msg.includes('RecordMeta')) {
192
+ return null;
193
+ }
194
+ throw err;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * List and decrypt all records owned by `ownerAddress`, with optional
200
+ * client-side filtering, sorting, pagination, and projection.
201
+ *
202
+ * ⚠ Records are decrypted client-side. Use `chainLimit` to constrain
203
+ * chain reads on large tables. For production scale, use the relay-
204
+ * maintained index endpoint (v1.2) instead.
205
+ */
206
+ async findMany(
207
+ ownerAddress : string,
208
+ options : FindManyOptions = {},
209
+ ): Promise<RecordWithId<Partial<T>>[]> {
210
+ const chainOffset = options.chainOffset ?? 0n;
211
+ const chainLimit = options.chainLimit ?? 200n;
212
+
213
+ const keys: string[] = await this.inner.listOwnerRecords(ownerAddress, chainOffset, chainLimit);
214
+ const raw: RecordWithId<T>[] = [];
215
+
216
+ // Decrypt in parallel (capped at 20 concurrent RPCs)
217
+ const BATCH = 20;
218
+ for (let i = 0; i < keys.length; i += BATCH) {
219
+ const batch = keys.slice(i, i + BATCH);
220
+ const settled = await Promise.allSettled(
221
+ batch.map(async (recordKey) => {
222
+ const plaintext = await this.inner.readPlaintext(recordKey);
223
+ return { id: 0n, data: this.decode(plaintext), recordKey };
224
+ }),
225
+ );
226
+ for (const r of settled) {
227
+ if (r.status === 'fulfilled') raw.push(r.value);
228
+ // silently skip inaccessible / deleted records
229
+ }
230
+ }
231
+
232
+ // Build query
233
+ let q = buildQuery(raw.map((r) => r.data as Record<string, unknown>));
234
+
235
+ if (options.where) {
236
+ for (const clause of options.where) {
237
+ if (clause[1] === 'isNull' || clause[1] === 'isNotNull') {
238
+ q = q.where(clause[0], clause[1]);
239
+ } else if (clause[1] === 'in' || clause[1] === 'notIn') {
240
+ q = q.where(clause[0], clause[1], clause[2] as unknown[]);
241
+ } else if (clause[1] === 'between') {
242
+ q = q.where(clause[0], clause[1], clause[2] as [unknown, unknown]);
243
+ } else {
244
+ q = q.where(clause[0], clause[1] as WhereOperator, clause[2]);
245
+ }
246
+ }
247
+ }
248
+ if (options.orderBy) {
249
+ for (const [field, dir] of options.orderBy) q = q.orderBy(field, dir ?? 'asc');
250
+ }
251
+ if (options.limit != null) q = q.limit(options.limit);
252
+ if (options.offset != null) q = q.offset(options.offset);
253
+ if (options.select) q = q.select(options.select);
254
+ if (options.distinct) q = q.distinct(options.distinct);
255
+
256
+ const filteredData = q.execute() as Partial<T>[];
257
+
258
+ // Remap back to RecordWithId — match by index since we decrypted in order
259
+ return filteredData.map((data, idx) => ({
260
+ id : 0n,
261
+ data,
262
+ recordKey: raw[idx]?.recordKey ?? '',
263
+ }));
264
+ }
265
+
266
+ /**
267
+ * Convenience: fetch and decrypt ALL records the wallet owns (no chain limit).
268
+ * Useful for small tables or full exports. Decrypts in parallel batches of 20.
269
+ */
270
+ async findAll(ownerAddress: string): Promise<RecordWithId<T>[]> {
271
+ const total = Number(await this.inner.ownerRecordCount(ownerAddress));
272
+ return this.findMany(ownerAddress, {
273
+ chainOffset: 0n,
274
+ chainLimit : BigInt(total),
275
+ }) as Promise<RecordWithId<T>[]>;
276
+ }
277
+
278
+ /**
279
+ * Aggregate over owner's records: COUNT, SUM, AVG, MIN, MAX, GROUP BY.
280
+ *
281
+ * @example
282
+ * await users.aggregate(ownerAddress, { count: '*', groupBy: 'role' })
283
+ * // => [{ group: 'admin', count: 3 }, { group: 'user', count: 47 }]
284
+ */
285
+ async aggregate(
286
+ ownerAddress : string,
287
+ opts : AggregateOptions,
288
+ where? : WhereTuple[],
289
+ chainLimit? : bigint,
290
+ ): Promise<AggregateResult[]> {
291
+ const records = await this.findMany(ownerAddress, {
292
+ chainLimit,
293
+ where,
294
+ });
295
+ const rows = records.map((r) => r.data as Record<string, unknown>);
296
+ return buildQuery(rows).aggregate(opts);
297
+ }
298
+
299
+ // ── Update ──────────────────────────────────────────────────
300
+
301
+ /**
302
+ * Fetch existing data, merge with `patch`, re-encrypt, and update on-chain.
303
+ */
304
+ async update(id: bigint, patch: Partial<T>): Promise<ethers.TransactionReceipt> {
305
+ const current = await this.findUnique(id);
306
+ if (current === null) throw new Error(`TypedTableClient.update: record ${id} not found`);
307
+ const merged = { ...current, ...patch } as T;
308
+ return this.inner.updateRaw(this.key(id), this.encode(merged));
309
+ }
310
+
311
+ /**
312
+ * Replace a record's data entirely (no merge).
313
+ * More gas-efficient when you have the full new payload ready.
314
+ */
315
+ async replace(id: bigint, data: T): Promise<ethers.TransactionReceipt> {
316
+ return this.inner.updateRaw(this.key(id), this.encode(data));
317
+ }
318
+
319
+ // ── Delete ──────────────────────────────────────────────────
320
+
321
+ /**
322
+ * Soft-delete a record. The symmetric key is scrubbed for all collaborators.
323
+ * Only the record owner can call this.
324
+ */
325
+ async remove(id: bigint): Promise<ethers.TransactionReceipt> {
326
+ return this.inner.deleteRecord(this.key(id));
327
+ }
328
+
329
+ // ── Helpers ─────────────────────────────────────────────────
330
+
331
+ /** Total number of records ever written by `ownerAddress` (including deleted). */
332
+ async count(ownerAddress: string): Promise<bigint> {
333
+ return this.inner.ownerRecordCount(ownerAddress);
334
+ }
335
+
336
+ /** True if a live (non-deleted) record exists for the given id. */
337
+ async exists(id: bigint): Promise<boolean> {
338
+ return this.inner.exists(this.key(id));
339
+ }
340
+ }
package/src/types.ts ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * @file types.ts
3
+ * @notice Web3QL v1.1 — extended type system with serialization, validation,
4
+ * DEFAULT/NOT NULL enforcement, and codec helpers.
5
+ *
6
+ * Every type maps: JS value ↔ JSON-storable "wire" representation.
7
+ * The chain stores the JSON string; this module handles encode/decode.
8
+ */
9
+
10
+ // ─────────────────────────────────────────────────────────────
11
+ // Field type literal union
12
+ // ─────────────────────────────────────────────────────────────
13
+
14
+ export type FieldType =
15
+ | 'INT' // bigint ↔ decimal string (int256)
16
+ | 'BIGINT' // bigint ↔ decimal string (uint256)
17
+ | 'UINT8' // number (0–255) ↔ number
18
+ | 'UINT16' // number (0–65535) ↔ number
19
+ | 'UINT32' // number (0–4294967295) ↔ number
20
+ | 'UINT64' // bigint ↔ decimal string
21
+ | 'TEXT' // string ↔ string
22
+ | 'BOOL' // boolean ↔ boolean
23
+ | 'FLOAT' // number ↔ number (stored scaled × 1e6 as integer)
24
+ | 'ADDRESS' // string (0x…) ↔ string, lowercased
25
+ | 'TIMESTAMP' // Date ↔ number (unix ms)
26
+ | 'DATE' // Date ↔ number (unix ms of midnight UTC)
27
+ | 'UUID' // string (36 chars) ↔ string
28
+ | 'BYTES32' // string (0x-prefixed, 66 chars) ↔ string
29
+ | 'JSON' // unknown ↔ unknown (validated as parseable JSON)
30
+ | 'JSONB' // alias for JSON — schema-less document storage
31
+ | 'ENUM' // string (label) ↔ number (index)
32
+ | 'DECIMAL'; // number ↔ string (exact decimal, no float rounding)
33
+
34
+ /** Sentinel value stored on-chain when a nullable field has no value. */
35
+ export const NULL_SENTINEL = '__NULL__';
36
+
37
+ // ─────────────────────────────────────────────────────────────
38
+ // Field descriptor
39
+ // ─────────────────────────────────────────────────────────────
40
+
41
+ export interface FieldDescriptor {
42
+ name : string;
43
+ type : FieldType;
44
+ primaryKey?: boolean;
45
+ notNull? : boolean;
46
+ /** Static default value OR factory function called on each write */
47
+ default? : unknown | (() => unknown);
48
+ /** ENUM: ordered list of string labels */
49
+ enumValues?: string[];
50
+ /** DECIMAL: [totalDigits, decimalPlaces] */
51
+ precision? : [number, number];
52
+ }
53
+
54
+ export type SchemaDefinition = FieldDescriptor[];
55
+
56
+ // ─────────────────────────────────────────────────────────────
57
+ // Type codecs (encode: JS → wire, decode: wire → JS)
58
+ // ─────────────────────────────────────────────────────────────
59
+
60
+ function encodeInt(v: unknown): number | string {
61
+ if (typeof v === 'bigint') return v.toString();
62
+ if (typeof v === 'number') return Math.trunc(v).toString();
63
+ return String(v);
64
+ }
65
+ function decodeInt(v: unknown): bigint { return BigInt(String(v)); }
66
+
67
+ function encodeFloat(v: unknown): number {
68
+ const n = Number(v);
69
+ if (!isFinite(n)) throw new TypeError(`FLOAT: expected finite number, got ${v}`);
70
+ // Store as scaled integer to avoid float drift
71
+ return Math.round(n * 1_000_000);
72
+ }
73
+ function decodeFloat(v: unknown): number { return Number(v) / 1_000_000; }
74
+
75
+ function encodeTimestamp(v: unknown): number {
76
+ if (v instanceof Date) return v.getTime();
77
+ if (typeof v === 'number') return v;
78
+ if (typeof v === 'string') return new Date(v).getTime();
79
+ throw new TypeError(`TIMESTAMP: expected Date or number, got ${typeof v}`);
80
+ }
81
+ function decodeTimestamp(v: unknown): Date { return new Date(Number(v)); }
82
+
83
+ function encodeDate(v: unknown): number {
84
+ const ms = encodeTimestamp(v);
85
+ // Truncate to midnight UTC
86
+ return ms - (ms % 86_400_000);
87
+ }
88
+ function decodeDate(v: unknown): Date {
89
+ const d = new Date(Number(v));
90
+ d.setUTCHours(0, 0, 0, 0);
91
+ return d;
92
+ }
93
+
94
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
95
+ function encodeUUID(v: unknown): string {
96
+ const s = String(v).toLowerCase();
97
+ if (!UUID_RE.test(s)) throw new TypeError(`UUID: invalid format "${s}"`);
98
+ return s;
99
+ }
100
+
101
+ const BYTES32_RE = /^0x[0-9a-f]{64}$/i;
102
+ function encodeBytes32(v: unknown): string {
103
+ const s = String(v).toLowerCase();
104
+ if (!BYTES32_RE.test(s)) throw new TypeError(`BYTES32: expected 0x-prefixed 64-hex-char string, got "${s}"`);
105
+ return s;
106
+ }
107
+
108
+ const ADDRESS_RE = /^0x[0-9a-f]{40}$/i;
109
+ function encodeAddress(v: unknown): string {
110
+ const s = String(v).toLowerCase();
111
+ if (!ADDRESS_RE.test(s)) throw new TypeError(`ADDRESS: invalid EVM address "${s}"`);
112
+ return s;
113
+ }
114
+
115
+ function encodeUint(bits: 8 | 16 | 32, v: unknown): number {
116
+ const n = Number(v);
117
+ const max = bits === 8 ? 255 : bits === 16 ? 65535 : 4294967295;
118
+ if (!Number.isInteger(n) || n < 0 || n > max) {
119
+ throw new RangeError(`UINT${bits}: value ${n} out of range [0, ${max}]`);
120
+ }
121
+ return n;
122
+ }
123
+
124
+ function encodeUint64(v: unknown): string {
125
+ const n = BigInt(String(v));
126
+ if (n < 0n) throw new RangeError(`UINT64: value must be non-negative`);
127
+ return n.toString();
128
+ }
129
+
130
+ function encodeJSON(v: unknown): unknown {
131
+ // Validate it round-trips cleanly
132
+ JSON.parse(JSON.stringify(v));
133
+ return v;
134
+ }
135
+
136
+ function encodeEnum(field: FieldDescriptor, v: unknown): number {
137
+ const labels = field.enumValues ?? [];
138
+ if (typeof v === 'number') {
139
+ if (v < 0 || v >= labels.length) throw new RangeError(`ENUM: index ${v} out of range [0,${labels.length})`);
140
+ return v;
141
+ }
142
+ const idx = labels.indexOf(String(v));
143
+ if (idx === -1) throw new TypeError(`ENUM: "${v}" not in [${labels.join(', ')}]`);
144
+ return idx;
145
+ }
146
+ function decodeEnum(field: FieldDescriptor, v: unknown): string {
147
+ const labels = field.enumValues ?? [];
148
+ const idx = Number(v);
149
+ return labels[idx] ?? String(v);
150
+ }
151
+
152
+ function encodeDecimal(field: FieldDescriptor, v: unknown): string {
153
+ const n = Number(v);
154
+ if (!isFinite(n)) throw new TypeError(`DECIMAL: expected finite number, got ${v}`);
155
+ const scale = field.precision?.[1] ?? 2;
156
+ return n.toFixed(scale);
157
+ }
158
+ function decodeDecimal(v: unknown): number { return parseFloat(String(v)); }
159
+
160
+ // ─────────────────────────────────────────────────────────────
161
+ // Public encode / decode helpers
162
+ // ─────────────────────────────────────────────────────────────
163
+
164
+ /** Encode a JS value → wire format (to be JSON-stringified and stored). */
165
+ export function encodeFieldValue(field: FieldDescriptor, value: unknown): unknown {
166
+ switch (field.type) {
167
+ case 'INT':
168
+ case 'BIGINT': return encodeInt(value);
169
+ case 'UINT8': return encodeUint(8, value);
170
+ case 'UINT16': return encodeUint(16, value);
171
+ case 'UINT32': return encodeUint(32, value);
172
+ case 'UINT64': return encodeUint64(value);
173
+ case 'TEXT': return String(value);
174
+ case 'BOOL': return Boolean(value);
175
+ case 'FLOAT': return encodeFloat(value);
176
+ case 'ADDRESS': return encodeAddress(value);
177
+ case 'TIMESTAMP': return encodeTimestamp(value);
178
+ case 'DATE': return encodeDate(value);
179
+ case 'UUID': return encodeUUID(value);
180
+ case 'BYTES32': return encodeBytes32(value);
181
+ case 'JSON':
182
+ case 'JSONB': return encodeJSON(value);
183
+ case 'ENUM': return encodeEnum(field, value);
184
+ case 'DECIMAL': return encodeDecimal(field, value);
185
+ }
186
+ }
187
+
188
+ /** Decode a wire-format value → typed JS value. */
189
+ export function decodeFieldValue(field: FieldDescriptor, value: unknown): unknown {
190
+ if (value === NULL_SENTINEL) return null;
191
+ if (value === null || value === undefined) return value;
192
+ switch (field.type) {
193
+ case 'INT':
194
+ case 'BIGINT': return decodeInt(value);
195
+ case 'UINT64': return BigInt(String(value));
196
+ case 'UINT8':
197
+ case 'UINT16':
198
+ case 'UINT32': return Number(value);
199
+ case 'TEXT':
200
+ case 'UUID':
201
+ case 'BYTES32':
202
+ case 'ADDRESS': return String(value);
203
+ case 'BOOL': return Boolean(value);
204
+ case 'FLOAT': return decodeFloat(value);
205
+ case 'TIMESTAMP': return decodeTimestamp(value);
206
+ case 'DATE': return decodeDate(value);
207
+ case 'JSON':
208
+ case 'JSONB': return value;
209
+ case 'ENUM': return decodeEnum(field, value);
210
+ case 'DECIMAL': return decodeDecimal(value);
211
+ }
212
+ }
213
+
214
+ // ─────────────────────────────────────────────────────────────
215
+ // Row-level validation — NOT NULL + DEFAULT + type coerce
216
+ // ─────────────────────────────────────────────────────────────
217
+
218
+ /**
219
+ * Validate and normalise a plain JS object before writing to the chain.
220
+ *
221
+ * • Applies DEFAULT values for missing fields
222
+ * • Enforces NOT NULL for fields without a default
223
+ * • Encodes each field to its wire representation
224
+ *
225
+ * @param schema SchemaDefinition for this table
226
+ * @param row Raw user input object
227
+ * @returns Wire-ready object (to be JSON.stringify'd and encrypted)
228
+ */
229
+ export function validateAndEncode(
230
+ schema: SchemaDefinition,
231
+ row : Record<string, unknown>,
232
+ ): Record<string, unknown> {
233
+ const out: Record<string, unknown> = {};
234
+
235
+ for (const field of schema) {
236
+ let value = row[field.name];
237
+
238
+ // Apply default if value is absent
239
+ if (value === undefined || value === null) {
240
+ if (field.default !== undefined) {
241
+ value = typeof field.default === 'function'
242
+ ? (field.default as () => unknown)()
243
+ : field.default;
244
+ }
245
+ }
246
+
247
+ // NOT NULL enforcement
248
+ if ((value === undefined || value === null) && field.notNull) {
249
+ throw new TypeError(
250
+ `Field "${field.name}" is NOT NULL but no value or default was provided.`
251
+ );
252
+ }
253
+
254
+ // Skip truly absent optional fields (store as NULL_SENTINEL for type safety)
255
+ if (value === undefined || value === null) {
256
+ out[field.name] = NULL_SENTINEL;
257
+ continue;
258
+ }
259
+
260
+ out[field.name] = encodeFieldValue(field, value);
261
+ }
262
+
263
+ return out;
264
+ }
265
+
266
+ /**
267
+ * Decode a wire-format JSON object back to typed JS values.
268
+ *
269
+ * @param schema SchemaDefinition for this table
270
+ * @param wire Object from JSON.parse(plaintext)
271
+ */
272
+ export function decodeRow(
273
+ schema: SchemaDefinition,
274
+ wire : Record<string, unknown>,
275
+ ): Record<string, unknown> {
276
+ const out: Record<string, unknown> = {};
277
+ const byName = new Map(schema.map((f) => [f.name, f]));
278
+
279
+ for (const [key, value] of Object.entries(wire)) {
280
+ const field = byName.get(key);
281
+ out[key] = field ? decodeFieldValue(field, value) : value;
282
+ }
283
+ return out;
284
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target" : "ES2022",
4
+ "module" : "Node16",
5
+ "moduleResolution" : "Node16",
6
+ "lib" : ["ES2022"],
7
+ "outDir" : "dist",
8
+ "rootDir" : ".",
9
+ "declaration" : true,
10
+ "declarationMap" : true,
11
+ "sourceMap" : true,
12
+ "esModuleInterop" : true,
13
+ "allowSyntheticDefaultImports": true,
14
+ "resolveJsonModule" : true,
15
+ "strict" : true,
16
+ "noUncheckedIndexedAccess" : true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "skipLibCheck" : true
19
+ },
20
+ "include": ["src/**/*.ts"],
21
+ "exclude": ["node_modules", "dist"]
22
+ }