Skip to content

Commit 65689ab

Browse files
committed
use StoreName type for compile-time store name validation
1 parent 9ef574f commit 65689ab

5 files changed

Lines changed: 54 additions & 64 deletions

File tree

src/storage/__tests__/storage.test.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,6 @@ function runStorageTests(name: string, createStorage: () => StorageBackend, clea
125125
expect(await storage.get('pool_leaves', 0)).toBeUndefined();
126126
});
127127

128-
it('throws on unknown store name (write)', async () => {
129-
await expect(storage.put('unknown_store', { id: 'a' })).rejects.toThrow('Unknown store');
130-
});
131-
132-
it('throws on unknown store name (read)', async () => {
133-
await expect(storage.get('unknown_store', 'a')).rejects.toThrow('Unknown store');
134-
});
135-
136128
it('throws on missing key field', async () => {
137129
await expect(storage.put('user_notes', { name: 'no id field' })).rejects.toThrow('Missing key field');
138130
});

src/storage/filesystem.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
22
import { join } from 'node:path';
3-
import { STORE_KEYS, type StorageBackend } from './storage.js';
3+
import { STORE_KEYS, type StorageBackend, type StoreName } from './storage.js';
44

55
export class FileSystemStorage implements StorageBackend {
66
constructor(private dirPath: string) {
@@ -9,53 +9,51 @@ export class FileSystemStorage implements StorageBackend {
99

1010
async init(): Promise<void> {}
1111

12-
private filePath(store: string): string {
13-
if (!STORE_KEYS[store]) throw new Error(`Unknown store: ${store}`);
12+
private filePath(store: StoreName): string {
1413
return join(this.dirPath, `${store}.json`);
1514
}
1615

17-
private readStore(store: string): Record<string, any> {
16+
private readStore(store: StoreName): Record<string, any> {
1817
const path = this.filePath(store);
1918
if (!existsSync(path)) return {};
2019
return JSON.parse(readFileSync(path, 'utf-8'));
2120
}
2221

23-
private writeStore(store: string, data: Record<string, any>): void {
22+
private writeStore(store: StoreName, data: Record<string, any>): void {
2423
writeFileSync(this.filePath(store), JSON.stringify(data, null, 2));
2524
}
2625

27-
private getKey(store: string, value: any): string {
26+
private getKey(store: StoreName, value: any): string {
2827
const keyField = STORE_KEYS[store];
29-
if (!keyField) throw new Error(`Unknown store: ${store}`);
3028
const key = value?.[keyField];
3129
if (key === undefined || key === null) throw new Error(`Missing key field '${keyField}' in value for store '${store}'`);
3230
return String(key);
3331
}
3432

35-
async get(store: string, key: any): Promise<any | undefined> {
33+
async get(store: StoreName, key: any): Promise<any | undefined> {
3634
const data = this.readStore(store);
3735
const k = String(key);
3836
return data[k] ? structuredClone(data[k]) : undefined;
3937
}
4038

41-
async getAll(store: string): Promise<any[]> {
39+
async getAll(store: StoreName): Promise<any[]> {
4240
return Object.values(this.readStore(store)).map(v => structuredClone(v));
4341
}
4442

45-
async getAllByIndex(store: string, index: string, value: any): Promise<any[]> {
43+
async getAllByIndex(store: StoreName, index: string, value: any): Promise<any[]> {
4644
return Object.values(this.readStore(store))
4745
.filter(record => record[index] === value)
4846
.map(v => structuredClone(v));
4947
}
5048

51-
async put(store: string, value: any): Promise<void> {
49+
async put(store: StoreName, value: any): Promise<void> {
5250
const data = this.readStore(store);
5351
const key = this.getKey(store, value);
5452
data[key] = structuredClone(value);
5553
this.writeStore(store, data);
5654
}
5755

58-
async putAll(store: string, values: any[]): Promise<void> {
56+
async putAll(store: StoreName, values: any[]): Promise<void> {
5957
const data = this.readStore(store);
6058
for (const value of values) {
6159
const key = this.getKey(store, value);
@@ -64,28 +62,28 @@ export class FileSystemStorage implements StorageBackend {
6462
this.writeStore(store, data);
6563
}
6664

67-
async del(store: string, key: any): Promise<void> {
65+
async del(store: StoreName, key: any): Promise<void> {
6866
const data = this.readStore(store);
6967
delete data[String(key)];
7068
this.writeStore(store, data);
7169
}
7270

73-
async clear(store: string): Promise<void> {
71+
async clear(store: StoreName): Promise<void> {
7472
this.writeStore(store, {});
7573
}
7674

7775
async clearAll(): Promise<void> {
78-
for (const storeName of Object.keys(STORE_KEYS)) {
76+
for (const storeName of Object.keys(STORE_KEYS) as StoreName[]) {
7977
const path = this.filePath(storeName);
8078
if (existsSync(path)) rmSync(path);
8179
}
8280
}
8381

84-
async count(store: string): Promise<number> {
82+
async count(store: StoreName): Promise<number> {
8583
return Object.keys(this.readStore(store)).length;
8684
}
8785

88-
async iterate(store: string, callback: (value: any) => boolean | void): Promise<void> {
86+
async iterate(store: StoreName, callback: (value: any) => boolean | void): Promise<void> {
8987
for (const value of Object.values(this.readStore(store))) {
9088
if (callback(structuredClone(value)) === false) break;
9189
}

src/storage/indexeddb.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { STORE_KEYS, type StorageBackend } from './storage.js';
1+
import { STORE_KEYS, type StorageBackend, type StoreName } from './storage.js';
22

33
const DB_NAME = 'private-payments-sdk';
44
const DB_VERSION = 1;
@@ -12,7 +12,7 @@ export class IndexedDBStorage implements StorageBackend {
1212
const request = indexedDB.open(DB_NAME, DB_VERSION);
1313
request.onupgradeneeded = () => {
1414
const db = request.result;
15-
for (const storeName of Object.keys(STORE_KEYS)) {
15+
for (const storeName of Object.keys(STORE_KEYS) as StoreName[]) {
1616
if (!db.objectStoreNames.contains(storeName)) {
1717
db.createObjectStore(storeName, { keyPath: STORE_KEYS[storeName] });
1818
}
@@ -24,11 +24,11 @@ export class IndexedDBStorage implements StorageBackend {
2424
}
2525

2626
private getDb(): IDBDatabase {
27-
if (!this.db) throw new Error('IndexedDBStorage not opened. Call open() first.');
27+
if (!this.db) throw new Error('IndexedDBStorage not initialized. Call init() first.');
2828
return this.db;
2929
}
3030

31-
private tx(store: string, mode: IDBTransactionMode): IDBObjectStore {
31+
private tx(store: StoreName, mode: IDBTransactionMode): IDBObjectStore {
3232
return this.getDb().transaction(store, mode).objectStore(store);
3333
}
3434

@@ -39,24 +39,24 @@ export class IndexedDBStorage implements StorageBackend {
3939
});
4040
}
4141

42-
async get(store: string, key: any): Promise<any | undefined> {
42+
async get(store: StoreName, key: any): Promise<any | undefined> {
4343
return this.req(this.tx(store, 'readonly').get(key));
4444
}
4545

46-
async getAll(store: string): Promise<any[]> {
46+
async getAll(store: StoreName): Promise<any[]> {
4747
return this.req(this.tx(store, 'readonly').getAll());
4848
}
4949

50-
async getAllByIndex(store: string, index: string, value: any): Promise<any[]> {
50+
async getAllByIndex(store: StoreName, index: string, value: any): Promise<any[]> {
5151
const all = await this.getAll(store);
5252
return all.filter(record => record[index] === value);
5353
}
5454

55-
async put(store: string, value: any): Promise<void> {
55+
async put(store: StoreName, value: any): Promise<void> {
5656
await this.req(this.tx(store, 'readwrite').put(value));
5757
}
5858

59-
async putAll(store: string, values: any[]): Promise<void> {
59+
async putAll(store: StoreName, values: any[]): Promise<void> {
6060
const tx = this.getDb().transaction(store, 'readwrite');
6161
const objectStore = tx.objectStore(store);
6262
for (const value of values) {
@@ -68,25 +68,25 @@ export class IndexedDBStorage implements StorageBackend {
6868
});
6969
}
7070

71-
async del(store: string, key: any): Promise<void> {
71+
async del(store: StoreName, key: any): Promise<void> {
7272
await this.req(this.tx(store, 'readwrite').delete(key));
7373
}
7474

75-
async clear(store: string): Promise<void> {
75+
async clear(store: StoreName): Promise<void> {
7676
await this.req(this.tx(store, 'readwrite').clear());
7777
}
7878

7979
async clearAll(): Promise<void> {
80-
for (const storeName of Object.keys(STORE_KEYS)) {
80+
for (const storeName of Object.keys(STORE_KEYS) as StoreName[]) {
8181
await this.clear(storeName);
8282
}
8383
}
8484

85-
async count(store: string): Promise<number> {
85+
async count(store: StoreName): Promise<number> {
8686
return this.req(this.tx(store, 'readonly').count());
8787
}
8888

89-
async iterate(store: string, callback: (value: any) => boolean | void): Promise<void> {
89+
async iterate(store: StoreName, callback: (value: any) => boolean | void): Promise<void> {
9090
return new Promise((resolve, reject) => {
9191
const request = this.tx(store, 'readonly').openCursor();
9292
request.onsuccess = () => {

src/storage/memory.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { STORE_KEYS, type StorageBackend } from './storage.js';
1+
import { STORE_KEYS, type StorageBackend, type StoreName } from './storage.js';
22

33
export class MemoryStorage implements StorageBackend {
44
private stores = new Map<string, Map<any, any>>();
55

66
async init(): Promise<void> {}
77

8-
private getStore(name: string): Map<any, any> {
9-
if (!STORE_KEYS[name]) throw new Error(`Unknown store: ${name}`);
8+
private getStore(name: StoreName): Map<any, any> {
109
let store = this.stores.get(name);
1110
if (!store) {
1211
store = new Map();
@@ -15,23 +14,22 @@ export class MemoryStorage implements StorageBackend {
1514
return store;
1615
}
1716

18-
private getKey(storeName: string, value: any): any {
17+
private getKey(storeName: StoreName, value: any): any {
1918
const keyField = STORE_KEYS[storeName];
20-
if (!keyField) throw new Error(`Unknown store: ${storeName}`);
2119
const key = value?.[keyField];
2220
if (key === undefined || key === null) throw new Error(`Missing key field '${keyField}' in value for store '${storeName}'`);
2321
return key;
2422
}
2523

26-
async get(store: string, key: any): Promise<any | undefined> {
24+
async get(store: StoreName, key: any): Promise<any | undefined> {
2725
return structuredClone(this.getStore(store).get(key));
2826
}
2927

30-
async getAll(store: string): Promise<any[]> {
28+
async getAll(store: StoreName): Promise<any[]> {
3129
return Array.from(this.getStore(store).values()).map(v => structuredClone(v));
3230
}
3331

34-
async getAllByIndex(store: string, index: string, value: any): Promise<any[]> {
32+
async getAllByIndex(store: StoreName, index: string, value: any): Promise<any[]> {
3533
const results: any[] = [];
3634
for (const record of this.getStore(store).values()) {
3735
if (record[index] === value) {
@@ -41,34 +39,34 @@ export class MemoryStorage implements StorageBackend {
4139
return results;
4240
}
4341

44-
async put(store: string, value: any): Promise<void> {
42+
async put(store: StoreName, value: any): Promise<void> {
4543
const key = this.getKey(store, value);
4644
this.getStore(store).set(key, structuredClone(value));
4745
}
4846

49-
async putAll(store: string, values: any[]): Promise<void> {
47+
async putAll(store: StoreName, values: any[]): Promise<void> {
5048
for (const value of values) {
5149
await this.put(store, value);
5250
}
5351
}
5452

55-
async del(store: string, key: any): Promise<void> {
53+
async del(store: StoreName, key: any): Promise<void> {
5654
this.getStore(store).delete(key);
5755
}
5856

59-
async clear(store: string): Promise<void> {
57+
async clear(store: StoreName): Promise<void> {
6058
this.getStore(store).clear();
6159
}
6260

6361
async clearAll(): Promise<void> {
6462
this.stores.clear();
6563
}
6664

67-
async count(store: string): Promise<number> {
65+
async count(store: StoreName): Promise<number> {
6866
return this.getStore(store).size;
6967
}
7068

71-
async iterate(store: string, callback: (value: any) => boolean | void): Promise<void> {
69+
async iterate(store: StoreName, callback: (value: any) => boolean | void): Promise<void> {
7270
for (const value of this.getStore(store).values()) {
7371
if (callback(structuredClone(value)) === false) break;
7472
}

src/storage/storage.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* - retention_config: { rpcEndpoint (key), windowLedgers, detectedAt }
2121
*/
2222
/** Primary key field for each store. Used by storage implementations to key records. */
23-
export const STORE_KEYS: Record<string, string> = {
23+
export const STORE_KEYS = {
2424
pool_leaves: 'index',
2525
pool_nullifiers: 'nullifier',
2626
pool_encrypted_outputs: 'commitment',
@@ -29,19 +29,21 @@ export const STORE_KEYS: Record<string, string> = {
2929
registered_public_keys: 'address',
3030
sync_metadata: 'network',
3131
retention_config: 'rpcEndpoint',
32-
};
32+
} as const;
33+
34+
export type StoreName = keyof typeof STORE_KEYS;
3335

3436
export interface StorageBackend {
3537
/** Initialize the storage backend (e.g., open IndexedDB connection). No-op for backends that don't need it. */
3638
init(): Promise<void>;
37-
get(store: string, key: any): Promise<any | undefined>;
38-
getAll(store: string): Promise<any[]>;
39-
getAllByIndex(store: string, index: string, value: any): Promise<any[]>;
40-
put(store: string, value: any): Promise<void>;
41-
putAll(store: string, values: any[]): Promise<void>;
42-
del(store: string, key: any): Promise<void>;
43-
clear(store: string): Promise<void>;
39+
get(store: StoreName, key: any): Promise<any | undefined>;
40+
getAll(store: StoreName): Promise<any[]>;
41+
getAllByIndex(store: StoreName, index: string, value: any): Promise<any[]>;
42+
put(store: StoreName, value: any): Promise<void>;
43+
putAll(store: StoreName, values: any[]): Promise<void>;
44+
del(store: StoreName, key: any): Promise<void>;
45+
clear(store: StoreName): Promise<void>;
4446
clearAll(): Promise<void>;
45-
count(store: string): Promise<number>;
46-
iterate(store: string, callback: (value: any) => boolean | void): Promise<void>;
47+
count(store: StoreName): Promise<number>;
48+
iterate(store: StoreName, callback: (value: any) => boolean | void): Promise<void>;
4749
}

0 commit comments

Comments
 (0)