Skip to content

[_]: feature/emails storage#37

Closed
xabg2 wants to merge 9 commits intomasterfrom
feature/emails-storage
Closed

[_]: feature/emails storage#37
xabg2 wants to merge 9 commits intomasterfrom
feature/emails-storage

Conversation

@xabg2
Copy link
Copy Markdown
Contributor

@xabg2 xabg2 commented Apr 7, 2026

Implementing the Emails Storage in the local Database. This PR contains:

  • The cryptography logic to encrypt/decrypt emails to save them in idb.
  • The email repository to fetch the data (a layer for DB service adapted to email store).
  • A Storage service to init the DB and use it to add, get, remove, etc items from the DB.

@xabg2 xabg2 requested a review from larryrider April 7, 2026 15:00
@xabg2 xabg2 self-assigned this Apr 7, 2026
@xabg2 xabg2 added the enhancement New feature or request label Apr 7, 2026
@xabg2 xabg2 marked this pull request as draft April 12, 2026 15:56
@xabg2 xabg2 closed this Apr 13, 2026
@xabg2 xabg2 reopened this Apr 13, 2026
@xabg2 xabg2 changed the base branch from feature/indexedDB to master April 13, 2026 12:28
@xabg2 xabg2 changed the title Feature/emails storage [_]: feature/emails storage Apr 13, 2026
@sonarqubecloud
Copy link
Copy Markdown

@xabg2 xabg2 marked this pull request as ready for review April 13, 2026 13:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

This PR refactors the database layer to support a multi-store architecture with generic, store-parameterized operations. It introduces email encryption abstraction (CryptoEmail), a repository pattern for email CRUD and search (EmailRepository), a provider singleton for lifecycle management (DatabaseProvider), new custom error classes, and restructured database configuration. The DatabaseService API is generalized from email-specific methods to generic store-scoped operations, and all dependent tests are refactored accordingly.

Changes

Cohort / File(s) Summary
Error Classes
src/errors/database/index.ts, src/errors/index.ts
Added three new exported error classes (MnemonicNotFoundError, ProviderNotInitializedError, EmailNotFoundError) extending Error with proper prototype inheritance, then re-exported via barrel.
Database Configuration
src/services/database/config.ts, src/services/database/config/index.ts, src/services/database/config/utils.ts
Removed monolithic config.ts and replaced with restructured config/index.ts exporting MAIL_DB_CONFIG with multi-index schema, plus new utility generateEmailParamPath for typed index path generation.
Type Definitions
src/services/database/types/index.ts
Restructured DatabaseConfig from single store to multi-store array (StoreConfig[]), changed EmailParams.folderId from optional string to optional string array, introduced Database interface for client API, and added EMAIL_DB_INDEXES_KEYS constant.
Database Service Core
src/services/database/index.ts
Refactored DatabaseService from email-specific fixed-store API to generic store-parameterized methods. Constructor changed from (userId, config) to (dbName, config). Replaced fixed methods (get, put, getAll, etc.) with generic equivalents requiring explicit store and indexName parameters. Split batching into getBatch and getBatchByFolder. Removed destroy() method and getDbName() helper.
Email Encryption
src/services/database/emails/crypto/index.ts, src/services/database/emails/crypto/crypto.test.ts
Added CryptoEmail class wrapping encryptEmailBodyWithKey and decryptEmailBody for transparent encrypt/decrypt operations on email subject and body fields, with full test coverage for encrypt and decrypt round-trips.
Email Repository
src/services/database/emails/repository/index.ts, src/services/database/emails/repository/email.repository.test.ts
Introduced EmailRepository with CRUD, bulk operations, index/range queries, cursor-based batching by folder and time, search with multi-filter support, and retention enforcement. Wraps database service and crypto layer. Includes comprehensive test suite covering encryption, database interaction, and search scenarios.
Database Provider
src/services/database/provider/index.ts, src/services/database/provider/database.provider.test.ts
Added DatabaseProvider singleton managing initialization, dependency wiring (database, crypto, repository), and lifecycle. Derives encryption key from mnemonic, validates user and mnemonic presence, and throws appropriate errors. Includes full test suite validating initialization, singleton behavior, and disconnection flow.
Test Refactoring
src/services/database/database.service.test.ts
Refactored database service tests to match new multi-store, generic API: replaced EMAIL_DB_CONFIG with MAIL_DB_CONFIG, updated CRUD/query calls with explicit STORE parameter, introduced createRecord/createRecordWithDate helpers, added index-based retrieval tests, and adjusted assertions for new record structure (params field).
Build Configuration
sonar-project.properties, tsconfig.app.json
Extended SonarQube coverage exclusions to omit src/**/config/** and **/*config.* files. Removed compilerOptions.baseUrl from TypeScript configuration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • larryrider
  • CandelR
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feature/emails storage' is related to the changeset but uses a generic feature branch naming convention rather than describing the actual changes made. Consider using a more descriptive title that highlights the main change, such as 'Add email encryption and repository layer for IndexedDB storage' or 'Implement encrypted email storage with database provider'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description accurately relates to the changeset by mentioning cryptography logic, email repository, and storage service initialization, all of which are present in the changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/emails-storage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/database/index.ts (1)

77-133: ⚠️ Potential issue | 🟠 Major

Cursor pagination drops rows when index keys are not unique.

Both methods store only cursor.key as the resume token, but when resuming across transactions with non-unique index keys, you must also store cursor.primaryKey. After reopening the cursor with the range, you need to skip records until you pass the previously-processed position. Without this, duplicate index values are skipped entirely.

For example, if three emails share the same receivedAt timestamp, the first batch retrieves two of them and returns that timestamp as nextCursor. The next batch uses IDBKeyRange.upperBound(timestamp, true), which in reverse direction skips all remaining records at that timestamp and continues to the next lower value, dropping the third email.

Resume with both the index key and primary key to avoid gaps:

  • Store cursor.key and cursor.primaryKey from the last processed record
  • Open the next cursor with IDBKeyRange.lowerBound(lastIndexKey, true) (or equivalent)
  • Skip records until you advance past the stored lastPrimaryKey
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/database/index.ts` around lines 77 - 133, getBatch and
getBatchByFolder currently resumecursors using only cursor.key which drops rows
when index keys are non-unique; change the resume token to capture both
cursor.key and cursor.primaryKey (e.g., lastIndexKey and lastPrimaryKey), reopen
the cursor using an appropriate bound (upperBound/lowerBound) on the index key
as you already do, then when iterating the reopened cursor skip records until
you encounter a record whose primaryKey is strictly past the saved
lastPrimaryKey (i.e., advance past duplicates with the same index key) before
collecting items; update the returned nextCursor to include both index key and
primaryKey so subsequent calls can resume correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/database/emails/crypto/crypto.test.ts`:
- Around line 6-12: The tests only cover the happy path for CryptoEmail.encrypt;
add edge-case tests that exercise CryptoEmail.decrypt failure modes: create a
CryptoEmail instance A with a key and encrypt a mail, then attempt to decrypt
with a different CryptoEmail instance B (wrong key) and assert it throws or
returns an invalid result; also tamper with the encrypted payload (e.g., flip
bytes in the ciphertext) and assert CryptoEmail.decrypt raises an error or fails
validation; include tests for empty/undefined inputs to
CryptoEmail.encrypt/decrypt to verify proper error handling in those methods.
- Around line 14-21: The test that calls new CryptoEmail(...),
crypto.encrypt(...) and crypto.decrypt(...) is asserting decryption but is
currently nested in the "Encrypt emails" describe; move this test out of that
describe block into a new or existing "Decrypt emails" describe (or rename the
test block) so tests are organized by behavior — locate the test that uses
CryptoEmail, encrypt and decrypt and relocate it under a describe('Decrypt
emails', ...) grouping without changing the test implementation.

In `@src/services/database/emails/repository/index.ts`:
- Around line 96-97: The byFrom and byTo index lookups
(EMAIL_DB_INDEXES_KEYS.byFrom / byTo used in the if branches) will never match
because params.from/params.to are arrays of User objects but the indexes were
not defined with multiEntry and/or a flattened string key; fix by either (A) add
a new index key on a flattened string array (e.g., store sender/recipient email
strings in a separate field like params.fromEmails/params.toEmails and define
the index with multiEntry: true) and query that index instead of byFrom/byTo, or
(B) remove these early index-based prefilters and fall back to using
matchesFilters(...) to filter sender/recipient in-memory; update the code paths
using STORE and EMAIL_DB_INDEXES_KEYS.byFrom/byTo (and any creation of
params.from/params.to) accordingly.
- Around line 93-95: The code is querying the byRead index with numeric 1/0
which won't match the boolean index; update the call to
this.db.getByIndex<StoredEmail>(STORE, EMAIL_DB_INDEXES_KEYS.byRead, ...) to
pass the boolean value directly (use filters.isRead) instead of converting to
1/0 so the IndexedDB index on params.isRead matches correctly.
- Around line 101-103: The date-range query in getBaseResults uses
IDBKeyRange.bound(filters.dateRange.after, filters.dateRange.before) which
creates numeric bounds that won't match the string ISO dates stored under
TIME_INDEX; update getBaseResults so the bounds match the stored type (either
convert filters.dateRange.after/ before to ISO strings before calling
IDBKeyRange.bound, or change the index to use numeric timestamps) and then call
this.db.getByRange<StoredEmail>(STORE, TIME_INDEX, range) with those corrected
bounds; refer to getBaseResults, filters.dateRange, IDBKeyRange.bound,
TIME_INDEX, StoredEmail and the existing dateInRange helper for guidance on
conversion.

In `@src/services/database/index.ts`:
- Around line 22-31: The upgrade currently only creates indexes when a store is
newly created, so new indexes added in later versions are skipped for existing
stores; modify the openDB upgrade callback (the upgrade function passed to
openDB) to accept the upgrade transaction (e.g., upgrade(db, oldVersion,
newVersion, tx)) and for each store in stores check: if the store already
exists, obtain the existing object store from the upgrade transaction
(tx.objectStore(store.name)) and iterate store.indexes to create any missing
indexes (check index presence via objectStore.indexNames.contains(index.name) or
similar) instead of only creating indexes on newly created object stores.

In `@src/services/database/provider/database.provider.test.ts`:
- Around line 44-49: The existing test only asserts sequential calls to
DatabaseProvider.init() return the same instance but misses the race-condition
scenario; add a new test that calls DatabaseProvider.init() concurrently (e.g.,
via Promise.all) and asserts both returned values are identical to ensure init()
handles concurrent callers and only creates a single instance; reference the
DatabaseProvider.init() method and add a test named like "When initializing
concurrently, then it should return the same instance".

In `@src/services/database/provider/index.ts`:
- Around line 26-38: The DatabaseService is opened before validating
prerequisites, so if LocalStorageService.instance.getMnemonic(),
deriveDatabaseKey(), or subsequent setup throws the db remains open; update the
init flow in the provider to either (A) gather mnemonic and compute indexKey
(using deriveDatabaseKey and mnemonicToBytes) and construct CryptoEmail and
EmailRepository before calling db.open(), or (B) wrap the post-open
initialization (derivation, new CryptoEmail, new EmailRepository, and setting
DatabaseProvider.instance) in a try/catch/finally and call db.close() in the
failure path; ensure you reference DatabaseService (db.open()/db.close()),
LocalStorageService.instance.getMnemonic(), MnemonicNotFoundError,
deriveDatabaseKey, CryptoEmail, EmailRepository, and DatabaseProvider.instance
when making the change so no open connection is leaked on error.
- Around line 20-38: DatabaseProvider.init can race when called concurrently;
make initialization atomic by introducing a static initPromise (e.g.,
DatabaseProvider.initPromise) and use it as a lock: at the top of init, if
DatabaseProvider.instance return it; if DatabaseProvider.initPromise exists
await and return it; otherwise set DatabaseProvider.initPromise to an async
initializer that performs the current creation steps (get user, open
DatabaseService, derive key, create CryptoEmail and EmailRepository), assigns
DatabaseProvider.instance, then clears initPromise (or leaves it resolved) and
returns the instance; ensure errors reject and clear initPromise so subsequent
calls can retry.

In `@src/services/database/types/index.ts`:
- Around line 77-101: The Database type must match the actual DatabaseService
surface: add the missing get<T>(storeName: string, key: IDBValidKey) =>
Promise<T | undefined> method and widen any parameters currently typed as string
| string[] to use IDBValidKey (e.g. the key parameter on getByIndex, remove, and
the startCursor on getBatch/getBatchByFolder) so callers (like EmailRepository)
can use the abstraction; keep getByRange as-is (IDBKeyRange) and ensure return
types (e.g. nextCursor) remain IDBValidKey.
- Around line 23-25: EmailParams currently defines folderId as string[] which
breaks queries against the byFolderIdAndTime compound index used by
getBatchByFolder because the index needs multiEntry semantics for array-valued
keys; either change EmailParams.folderId from string[] to string if emails only
ever belong to a single folder, or update the byFolderIdAndTime index definition
to include multiEntry: true (in the index config where keyPath is
['params.folderId','params.receivedAt']) so array folder IDs are indexed
per-entry and queries using scalar folderId strings will match.

---

Outside diff comments:
In `@src/services/database/index.ts`:
- Around line 77-133: getBatch and getBatchByFolder currently resumecursors
using only cursor.key which drops rows when index keys are non-unique; change
the resume token to capture both cursor.key and cursor.primaryKey (e.g.,
lastIndexKey and lastPrimaryKey), reopen the cursor using an appropriate bound
(upperBound/lowerBound) on the index key as you already do, then when iterating
the reopened cursor skip records until you encounter a record whose primaryKey
is strictly past the saved lastPrimaryKey (i.e., advance past duplicates with
the same index key) before collecting items; update the returned nextCursor to
include both index key and primaryKey so subsequent calls can resume correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ad638d81-e9ec-4a6f-ab73-689facb91b05

📥 Commits

Reviewing files that changed from the base of the PR and between 2a2e50a and 1b10367.

📒 Files selected for processing (16)
  • sonar-project.properties
  • src/errors/database/index.ts
  • src/errors/index.ts
  • src/services/database/config.ts
  • src/services/database/config/index.ts
  • src/services/database/config/utils.ts
  • src/services/database/database.service.test.ts
  • src/services/database/emails/crypto/crypto.test.ts
  • src/services/database/emails/crypto/index.ts
  • src/services/database/emails/repository/email.repository.test.ts
  • src/services/database/emails/repository/index.ts
  • src/services/database/index.ts
  • src/services/database/provider/database.provider.test.ts
  • src/services/database/provider/index.ts
  • src/services/database/types/index.ts
  • tsconfig.app.json
💤 Files with no reviewable changes (2)
  • tsconfig.app.json
  • src/services/database/config.ts

Comment thread src/services/database/emails/crypto/crypto.test.ts
Comment thread src/services/database/emails/crypto/crypto.test.ts
Comment on lines +93 to +95
if (filters.isRead !== undefined) {
return this.db.getByIndex<StoredEmail>(STORE, EMAIL_DB_INDEXES_KEYS.byRead, filters.isRead ? 1 : 0);
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In IndexedDB, if an index keyPath resolves to a boolean property, does IDBIndex.getAll need true/false, or can numeric 1/0 match?

💡 Result:

In IndexedDB, if an index keyPath resolves to a boolean property (true or false), IDBIndex.getAll requires the boolean values true or false for matching. Numeric 1/0 cannot match because booleans are not valid IndexedDB keys. Attempting to create an index on a boolean property or use a boolean as a query key will fail with a DataError DOMException, as the key is invalid. To query records where the boolean property is true/false, you cannot use the index directly. Instead, use a cursor on the object store without a key range (full scan) and filter in JavaScript, or store a numeric equivalent (1/0) in a separate indexed property.

Citations:


🏁 Script executed:

# Find the repository index file and examine it
cat -n src/services/database/emails/repository/index.ts | head -150

Repository: internxt/mail-web

Length of output: 6541


🏁 Script executed:

# Search for IndexedDB configuration, schema, or setup files
find . -type f \( -name "*.ts" -o -name "*.js" \) | xargs rg -l "byRead|EMAIL_DB_INDEXES" | head -20

Repository: internxt/mail-web

Length of output: 257


🏁 Script executed:

# Look for index key definitions
rg "EMAIL_DB_INDEXES_KEYS|byRead" -A 3 -B 3

Repository: internxt/mail-web

Length of output: 11766


🏁 Script executed:

# Check the StoredEmail type definition
rg "type StoredEmail|interface StoredEmail" -A 15 src/services/database/types/

Repository: internxt/mail-web

Length of output: 919


🏁 Script executed:

# Check generateEmailParamPath implementation
cat -n src/services/database/config/utils.ts 2>/dev/null || rg "generateEmailParamPath" -B 5 -A 10

Repository: internxt/mail-web

Length of output: 262


🏁 Script executed:

# Find DatabaseService implementation
rg "class DatabaseService|getByIndex" src/services/database/ -B 2 -A 10

Repository: internxt/mail-web

Length of output: 13783


Query byRead with booleans, not 1/0.

The byRead index is created on params.isRead which is a boolean field. IndexedDB requires exact type matching for keys—querying with numeric 1 or 0 will never match boolean true or false values in the index. Pass the boolean directly: filters.isRead instead of converting to 1/0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/database/emails/repository/index.ts` around lines 93 - 95, The
code is querying the byRead index with numeric 1/0 which won't match the boolean
index; update the call to this.db.getByIndex<StoredEmail>(STORE,
EMAIL_DB_INDEXES_KEYS.byRead, ...) to pass the boolean value directly (use
filters.isRead) instead of converting to 1/0 so the IndexedDB index on
params.isRead matches correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that IDBValidKey does not return a boolean field:
type IDBValidKey = number | string | Date | BufferSource | IDBValidKey[];

So we need to use numbers as we do, right?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the EmailParams interface definition
rg "EmailParams|isRead" src/services/database/types/ -A 2 -B 2 -n

# Also check the StoredEmail type
rg "StoredEmail|EmailParams" src/services/database/types/ -A 20 -n

Length of output: 2904


@xabg2, you're absolutely right on the TypeScript side — IDBValidKey excludes boolean, so passing 1/0 is indeed the only valid approach for the query argument, and my original suggestion to pass filters.isRead directly would have been a TypeScript type error. I apologize for the incorrect suggestion.

However, there is still a real issue on the storage side. EmailParams.isRead is typed as boolean (Line 25 in src/services/database/types/index.ts):

export interface EmailParams {
  isRead: boolean;  // <-- boolean stored in IDB
  ...
}

Since boolean is not a valid IndexedDB key type, IndexedDB silently skips records whose index keyPath resolves to a boolean — they are simply not added to the byRead index. This means even though your query getByIndex(STORE, byRead, 1) is TypeScript-valid, it will always return an empty array at runtime because no records ever populate that index.

The fix would be to change the isRead field in EmailParams to a numeric type and ensure it's stored as 1/0:

export interface EmailParams {
  isRead: 0 | 1;  // valid IDB key type
  receivedAt: string;
  ...
}

And update all writes accordingly (e.g. isRead: email.isRead ? 1 : 0). The same concern applies to hasAttachment: boolean (Line 29) if it is used as an index key.


✏️ Learnings added
Learnt from: xabg2
Repo: internxt/mail-web PR: 37
File: src/services/database/emails/repository/index.ts:93-95
Timestamp: 2026-04-13T14:02:25.315Z
Learning: In the internxt/mail-web repository, `EmailParams.isRead` (in `src/services/database/types/index.ts`) is typed as `boolean`. Since boolean is not a valid IndexedDB key type (`IDBValidKey = number | string | Date | BufferSource | IDBValidKey[]`), IndexedDB silently skips records where the index keyPath resolves to a boolean, making the `byRead` index effectively empty. The correct fix is to store `isRead` as `0 | 1` (number) rather than boolean, and convert at write time. Querying with numeric `1/0` in `getByIndex` is the correct TypeScript approach since `IDBValidKey` excludes boolean.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread src/services/database/emails/repository/index.ts
Comment thread src/services/database/emails/repository/index.ts
Comment thread src/services/database/provider/database.provider.test.ts
Comment on lines +20 to +38
static async init(): Promise<DatabaseProvider> {
if (DatabaseProvider.instance) return DatabaseProvider.instance;

const user = LocalStorageService.instance.getUser();
if (!user) throw new UserNotFoundError();

const databaseName = `DB:${user.uuid}`;
const db = new DatabaseService(databaseName, MAIL_DB_CONFIG);
await db.open();

const mnemonic = LocalStorageService.instance.getMnemonic();
if (!mnemonic) throw new MnemonicNotFoundError();
const indexKey = await deriveDatabaseKey(mnemonicToBytes(mnemonic));
const crypto = new CryptoEmail(indexKey);

const emails = new EmailRepository(db, crypto);

DatabaseProvider.instance = new DatabaseProvider(db, emails);
return DatabaseProvider.instance;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make singleton initialization atomic.

Two parallel init() calls can both pass the instance check, open separate databases, and then race to overwrite the singleton. Please guard initialization with a cached initPromise or equivalent lock.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/database/provider/index.ts` around lines 20 - 38,
DatabaseProvider.init can race when called concurrently; make initialization
atomic by introducing a static initPromise (e.g., DatabaseProvider.initPromise)
and use it as a lock: at the top of init, if DatabaseProvider.instance return
it; if DatabaseProvider.initPromise exists await and return it; otherwise set
DatabaseProvider.initPromise to an async initializer that performs the current
creation steps (get user, open DatabaseService, derive key, create CryptoEmail
and EmailRepository), assigns DatabaseProvider.instance, then clears initPromise
(or leaves it resolved) and returns the instance; ensure errors reject and clear
initPromise so subsequent calls can retry.

Comment thread src/services/database/provider/index.ts
Comment thread src/services/database/types/index.ts
Comment on lines +77 to +101
export type Database = {
open: () => Promise<void>;
close: () => void;
getAll: <T>(storeName: string) => Promise<T[]>;
getByIndex: <T>(storeName: string, indexName: string, key: string | string[]) => Promise<T[]>;
getByRange: <T>(storeName: string, indexName: string, range: IDBKeyRange) => Promise<T[]>;
put: <T>(storeName: string, record: T) => Promise<IDBValidKey>;
putMany: <T>(storeName: string, records: T[]) => Promise<void>;
remove: (storeName: string, key: string | string[]) => Promise<void>;
count: (storeName: string) => Promise<number>;
deleteOldest: (storeName: string, indexName: string, count: number) => Promise<void>;
getBatch: <T>(
storeName: string,
indexName: string,
batchSize: number,
startCursor?: string | string[],
) => Promise<{ items: T[]; nextCursor?: IDBValidKey }>;
getBatchByFolder: <T>(
storeName: string,
indexName: string,
folderId: string,
batchSize: number,
startCursor?: IDBValidKey,
) => Promise<{ items: T[]; nextCursor?: IDBValidKey }>;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

The public Database contract no longer matches the service surface.

DatabaseService exposes get() and accepts general IDBValidKey keys/cursors, but this type omits get() and narrows several parameters to string | string[]. That forces EmailRepository and its tests to depend on the concrete class instead of the abstraction. Please mirror the actual service signatures here and inject Database downstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/database/types/index.ts` around lines 77 - 101, The Database
type must match the actual DatabaseService surface: add the missing
get<T>(storeName: string, key: IDBValidKey) => Promise<T | undefined> method and
widen any parameters currently typed as string | string[] to use IDBValidKey
(e.g. the key parameter on getByIndex, remove, and the startCursor on
getBatch/getBatchByFolder) so callers (like EmailRepository) can use the
abstraction; keep getByRange as-is (IDBKeyRange) and ensure return types (e.g.
nextCursor) remain IDBValidKey.

@xabg2
Copy link
Copy Markdown
Contributor Author

xabg2 commented Apr 14, 2026

I'm going to close this PR as we are doing to do the search on the server side. We only need to handle the encrypted mails, but they will not need all this indexes. I will only refactor the database service and that's it.

@xabg2 xabg2 closed this Apr 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant