Plaintext values on encryption

This commit is contained in:
2026-02-25 15:35:04 +11:00
parent f80aa2dcfc
commit 77593fe3b4
8 changed files with 407 additions and 143 deletions

View File

@@ -1,4 +1,11 @@
import { BaseStorage, FindOptions, type IndexDefinition } from './base-storage.js';
import {
BaseStorage,
FindOptions,
type IndexDefinition,
type Filter,
type ComparisonOperators,
isOperatorObject,
} from './base-storage.js';
/**
* Separator used when joining multiple field values into a single index key.
@@ -86,7 +93,7 @@ export class StorageMemory<
}
}
async find(filter?: Partial<T>, options?: FindOptions): Promise<T[]> {
async find(filter?: Filter<T>, options?: FindOptions): Promise<T[]> {
let results: T[];
// Attempt to satisfy the query via an index.
@@ -118,7 +125,7 @@ export class StorageMemory<
}
async updateMany(
filter: Partial<T>,
filter: Filter<T>,
update: Partial<T>,
options: Partial<FindOptions> = {},
): Promise<number> {
@@ -147,7 +154,7 @@ export class StorageMemory<
}
async deleteMany(
filter: Partial<T>,
filter: Filter<T>,
options: Partial<FindOptions> = {},
): Promise<number> {
const rowsToDelete = this.collectMatches(filter);
@@ -182,26 +189,42 @@ export class StorageMemory<
/**
* Checks whether a document satisfies every field in the filter.
* An empty or undefined filter matches everything.
* Supports both plain equality values and comparison operator objects.
*/
private matchesFilter(item: T, filter?: Partial<T>): boolean {
private matchesFilter(item: T, filter?: Filter<T>): boolean {
if (!filter || Object.keys(filter).length === 0) {
return true;
}
for (const [key, value] of Object.entries(filter)) {
if (item[key] !== value) {
return false;
if (isOperatorObject(value)) {
if (!this.matchesOperators(item[key], value)) return false;
} else {
if (item[key] !== value) return false;
}
}
return true;
}
/**
* Evaluate a set of comparison operators against a single field value.
* All operators must pass for the field to match.
*/
private matchesOperators(fieldValue: any, ops: ComparisonOperators<any>): boolean {
if (ops.$eq !== undefined && fieldValue !== ops.$eq) return false;
if (ops.$ne !== undefined && fieldValue === ops.$ne) return false;
if (ops.$lt !== undefined && !(fieldValue < ops.$lt)) return false;
if (ops.$lte !== undefined && !(fieldValue <= ops.$lte)) return false;
if (ops.$gt !== undefined && !(fieldValue > ops.$gt)) return false;
if (ops.$gte !== undefined && !(fieldValue >= ops.$gte)) return false;
return true;
}
/**
* Collect all [internalKey, document] pairs that match a filter.
* Uses an index when possible, otherwise falls back to a full scan.
*/
private collectMatches(filter?: Partial<T>): Array<[number, T]> {
private collectMatches(filter?: Filter<T>): Array<[number, T]> {
const indexKeys = this.resolveIndexKeys(filter);
if (indexKeys !== null) {
@@ -296,21 +319,31 @@ export class StorageMemory<
* Attempt to resolve a set of candidate internal keys from the indexes.
* Returns `null` if no index can serve the query.
*
* An index is used when the filter fields are a superset of (or equal to)
* an index's fields — meaning the index value can be fully constructed
* from the filter.
* An index is used when the filter contains plain equality values for every
* field in the index. Operator objects (e.g. `{ $lt: 50 }`) are excluded
* from index resolution since hash-based indexes only support equality.
*/
private resolveIndexKeys(filter?: Partial<T>): Set<number> | null {
private resolveIndexKeys(filter?: Filter<T>): Set<number> | null {
if (!filter) return null;
const filterKeys = Object.keys(filter);
if (filterKeys.length === 0) return null;
// Extract only the equality fields from the filter (skip operator objects).
const equalityFilter: Record<string, any> = {};
for (const [key, value] of Object.entries(filter)) {
if (!isOperatorObject(value)) {
equalityFilter[key] = value;
}
}
if (Object.keys(equalityFilter).length === 0) return null;
for (const fields of this.indexDefs) {
// Every field in the index must be present in the filter.
if (!fields.every((f) => f in filter)) continue;
// Every field in the index must be present as an equality value.
if (!fields.every((f) => f in equalityFilter)) continue;
const indexName = fields.join(INDEX_KEY_SEP);
const indexValue = this.buildIndexValue(filter, fields);
const indexValue = this.buildIndexValue(equalityFilter, fields);
if (indexValue === null) continue;
const indexMap = this.indexes.get(indexName)!;
@@ -326,7 +359,7 @@ export class StorageMemory<
* Returns `null` when no index can serve the filter, signalling
* the caller to fall back to a full scan.
*/
private findViaIndex(filter?: Partial<T>): T[] | null {
private findViaIndex(filter?: Filter<T>): T[] | null {
const keys = this.resolveIndexKeys(filter);
if (keys === null) return null;