Plaintext values on encryption
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user