refactor: simplify and add memory indexing
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { hashCodeQuery } from "../../hash.ts";
|
||||
import type { QueryOptions } from "../../storage/mod.ts";
|
||||
import type { Document, Filter } from "../../types.ts";
|
||||
import type { Criteria } from "mingo/types";
|
||||
|
||||
export class IndexedDBCache<TSchema extends Document = Document> {
|
||||
import { hashCodeQuery } from "../../hash.ts";
|
||||
import type { QueryOptions } from "../../storage.ts";
|
||||
import type { AnyDocument } from "../../types.ts";
|
||||
|
||||
export class IndexedDBCache<TSchema extends AnyDocument = AnyDocument> {
|
||||
readonly #cache = new Map<number, string[]>();
|
||||
readonly #documents = new Map<string, TSchema>();
|
||||
|
||||
hash(filter: Filter<TSchema>, options: QueryOptions): number {
|
||||
return hashCodeQuery(filter, options);
|
||||
hash(condition: Criteria<TSchema>, options: QueryOptions = {}): number {
|
||||
return hashCodeQuery(condition, options);
|
||||
}
|
||||
|
||||
set(hashCode: number, documents: TSchema[]) {
|
||||
@@ -23,7 +25,7 @@ export class IndexedDBCache<TSchema extends Document = Document> {
|
||||
get(hashCode: number): TSchema[] | undefined {
|
||||
const ids = this.#cache.get(hashCode);
|
||||
if (ids !== undefined) {
|
||||
return ids.map((id) => this.#documents.get(id) as TSchema);
|
||||
return ids.map((id) => this.#documents.get(id)).filter((document) => document !== undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,30 +2,33 @@ import { type IDBPDatabase, openDB } from "idb";
|
||||
|
||||
import { Collection } from "../../collection.ts";
|
||||
import type { DBLogger } from "../../logger.ts";
|
||||
import type { Document } from "../../types.ts";
|
||||
import type { Registrars } from "../registrars.ts";
|
||||
import type { Index, Registrars } from "../../registrars.ts";
|
||||
import { IndexedDBStorage } from "./storage.ts";
|
||||
|
||||
export class IndexedDB<TCollections extends StringRecord<Document>> {
|
||||
readonly #collections = new Map<keyof TCollections, Collection<TCollections[keyof TCollections]>>();
|
||||
export class IndexedDB<TOptions extends IndexedDBOptions> {
|
||||
readonly #collections = new Map<string, Collection>();
|
||||
readonly #db: Promise<IDBPDatabase<unknown>>;
|
||||
|
||||
constructor(readonly options: Options) {
|
||||
constructor(readonly options: TOptions) {
|
||||
this.#db = openDB(options.name, options.version ?? 1, {
|
||||
upgrade: (db: IDBPDatabase) => {
|
||||
for (const { name, primaryKey = "id", indexes = [] } of options.registrars) {
|
||||
const store = db.createObjectStore(name as string, { keyPath: primaryKey });
|
||||
store.createIndex(primaryKey, primaryKey, { unique: true });
|
||||
for (const { name, indexes = [] } of options.registrars) {
|
||||
const store = db.createObjectStore(name);
|
||||
for (const [keyPath, options] of indexes) {
|
||||
store.createIndex(keyPath, keyPath, options);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
for (const { name, primaryKey = "id" } of options.registrars) {
|
||||
for (const { name, schema, indexes } of options.registrars) {
|
||||
this.#collections.set(
|
||||
name,
|
||||
new Collection(name, new IndexedDBStorage(name, primaryKey, this.#db, options.log ?? log)),
|
||||
new Collection({
|
||||
name,
|
||||
storage: new IndexedDBStorage(name, indexes, this.#db, options.log),
|
||||
schema,
|
||||
indexes,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +39,17 @@ export class IndexedDB<TCollections extends StringRecord<Document>> {
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
collection<Name extends keyof TCollections = keyof TCollections>(name: Name) {
|
||||
collection<
|
||||
TName extends TOptions["registrars"][number]["name"],
|
||||
TSchema = Extract<TOptions["registrars"][number], { name: TName }>["schema"],
|
||||
>(
|
||||
name: TName,
|
||||
): Collection<{
|
||||
name: TName;
|
||||
storage: IndexedDBStorage;
|
||||
schema: TSchema;
|
||||
indexes: Index[];
|
||||
}> {
|
||||
const collection = this.#collections.get(name);
|
||||
if (collection === undefined) {
|
||||
throw new Error(`Collection '${name as string}' not found`);
|
||||
@@ -65,13 +78,9 @@ export class IndexedDB<TCollections extends StringRecord<Document>> {
|
||||
}
|
||||
}
|
||||
|
||||
function log() {}
|
||||
|
||||
type StringRecord<TCollections> = { [x: string]: TCollections };
|
||||
|
||||
type Options = {
|
||||
type IndexedDBOptions<TRegistrars extends Array<Registrars> = Array<any>> = {
|
||||
name: string;
|
||||
registrars: TRegistrars;
|
||||
version?: number;
|
||||
registrars: Registrars[];
|
||||
log?: DBLogger;
|
||||
};
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
import type { IDBPDatabase } from "idb";
|
||||
import { Query, update } from "mingo";
|
||||
import type { Criteria, Options } from "mingo/types";
|
||||
import type { CloneMode, Modifier } from "mingo/updater";
|
||||
import type { Criteria } from "mingo/types";
|
||||
import type { Modifier } from "mingo/updater";
|
||||
|
||||
import { type DBLogger, InsertLog, QueryLog, RemoveLog, ReplaceLog, UpdateLog } from "../../logger.ts";
|
||||
import { getDocumentWithPrimaryKey } from "../../primary-key.ts";
|
||||
import { DuplicateDocumentError } from "../../storage/errors.ts";
|
||||
import {
|
||||
getInsertManyResult,
|
||||
getInsertOneResult,
|
||||
type InsertManyResult,
|
||||
type InsertOneResult,
|
||||
} from "../../storage/operators/insert.ts";
|
||||
import { RemoveResult } from "../../storage/operators/remove.ts";
|
||||
import { UpdateResult } from "../../storage/operators/update.ts";
|
||||
import { addOptions, type Index, type QueryOptions, Storage } from "../../storage/storage.ts";
|
||||
import type { Document, Filter } from "../../types.ts";
|
||||
import { type DBLogger, InsertLog, QueryLog, RemoveLog, UpdateLog } from "../../logger.ts";
|
||||
import type { Index } from "../../registrars.ts";
|
||||
import { addOptions, type QueryOptions, Storage, type UpdateResult } from "../../storage.ts";
|
||||
import type { AnyDocument } from "../../types.ts";
|
||||
import { IndexedDBCache } from "./cache.ts";
|
||||
|
||||
const OBJECT_PROTOTYPE = Object.getPrototypeOf({});
|
||||
const OBJECT_TAG = "[object Object]";
|
||||
|
||||
export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Document = Document> extends Storage<
|
||||
TPrimaryKey,
|
||||
TSchema
|
||||
> {
|
||||
export class IndexedDBStorage<TSchema extends AnyDocument = AnyDocument> extends Storage<TSchema> {
|
||||
readonly #cache = new IndexedDBCache<TSchema>();
|
||||
|
||||
readonly #promise: Promise<IDBPDatabase>;
|
||||
@@ -33,11 +21,11 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
primaryKey: TPrimaryKey,
|
||||
indexes: Index[],
|
||||
promise: Promise<IDBPDatabase>,
|
||||
readonly log: DBLogger,
|
||||
readonly log: DBLogger = function log() {},
|
||||
) {
|
||||
super(name, primaryKey);
|
||||
super(name, indexes);
|
||||
this.#promise = promise;
|
||||
}
|
||||
|
||||
@@ -69,45 +57,17 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async insertOne(values: TSchema | Omit<TSchema, TPrimaryKey>): Promise<InsertOneResult> {
|
||||
async insert(documents: TSchema[]): Promise<void> {
|
||||
const logger = new InsertLog(this.name);
|
||||
|
||||
const document = getDocumentWithPrimaryKey(this.primaryKey, values);
|
||||
|
||||
if (await this.has(document[this.primaryKey])) {
|
||||
throw new DuplicateDocumentError(document, this as any);
|
||||
}
|
||||
await this.db.transaction(this.name, "readwrite", { durability: "relaxed" }).store.add(document);
|
||||
|
||||
this.broadcast("insertOne", document);
|
||||
this.#cache.flush();
|
||||
|
||||
this.log(logger.result());
|
||||
|
||||
return getInsertOneResult(document);
|
||||
}
|
||||
|
||||
async insertMany(values: (TSchema | Omit<TSchema, TPrimaryKey>)[]): Promise<InsertManyResult> {
|
||||
const logger = new InsertLog(this.name);
|
||||
|
||||
const documents: TSchema[] = [];
|
||||
|
||||
const tx = this.db.transaction(this.name, "readwrite", { durability: "relaxed" });
|
||||
await Promise.all(
|
||||
values.map((values) => {
|
||||
const document = getDocumentWithPrimaryKey(this.primaryKey, values);
|
||||
documents.push(document);
|
||||
return tx.store.add(document);
|
||||
}),
|
||||
);
|
||||
await Promise.all(documents.map((document) => tx.store.add(document)));
|
||||
await tx.done;
|
||||
|
||||
this.broadcast("insertMany", documents);
|
||||
this.broadcast("insert", documents);
|
||||
this.#cache.flush();
|
||||
|
||||
this.log(logger.result());
|
||||
|
||||
return getInsertManyResult(documents);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -116,28 +76,28 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async findById(id: string): Promise<TSchema | undefined> {
|
||||
return this.db.getFromIndex(this.name, "id", id);
|
||||
async getByIndex(index: string, value: string): Promise<TSchema[]> {
|
||||
return this.db.getAllFromIndex(this.name, index, value);
|
||||
}
|
||||
|
||||
async find(filter: Filter<TSchema>, options: QueryOptions = {}): Promise<TSchema[]> {
|
||||
const logger = new QueryLog(this.name, { filter, options });
|
||||
async find(condition: Criteria<TSchema> = {}, options?: QueryOptions): Promise<TSchema[]> {
|
||||
const logger = new QueryLog(this.name, { condition, options });
|
||||
|
||||
const hashCode = this.#cache.hash(filter, options);
|
||||
const hashCode = this.#cache.hash(condition, options);
|
||||
const cached = this.#cache.get(hashCode);
|
||||
if (cached !== undefined) {
|
||||
this.log(logger.result({ cached: true }));
|
||||
return cached;
|
||||
}
|
||||
|
||||
const indexes = this.#resolveIndexes(filter);
|
||||
let cursor = new Query(filter).find<TSchema>(await this.#getAll({ ...options, ...indexes }));
|
||||
const indexes = this.#resolveIndexes(condition);
|
||||
let cursor = new Query(condition).find<TSchema>(await this.#getAll({ ...options, ...indexes }));
|
||||
if (options !== undefined) {
|
||||
cursor = addOptions(cursor, options);
|
||||
}
|
||||
|
||||
const documents = cursor.all() as TSchema[];
|
||||
this.#cache.set(this.#cache.hash(filter, options), documents);
|
||||
const documents = cursor.all();
|
||||
this.#cache.set(this.#cache.hash(condition, options), documents);
|
||||
|
||||
this.log(logger.result());
|
||||
|
||||
@@ -172,10 +132,7 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
return {};
|
||||
}
|
||||
|
||||
async #getAll({ index, offset, range, limit }: QueryOptions) {
|
||||
if (index !== undefined) {
|
||||
return this.#getAllByIndex(index);
|
||||
}
|
||||
async #getAll({ offset, range, limit }: QueryOptions) {
|
||||
if (range !== undefined) {
|
||||
return this.db.getAll(this.name, IDBKeyRange.bound(range.from, range.to));
|
||||
}
|
||||
@@ -185,23 +142,6 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
return this.db.getAll(this.name, undefined, limit);
|
||||
}
|
||||
|
||||
async #getAllByIndex(index: Index) {
|
||||
let result = new Set();
|
||||
for (const key in index) {
|
||||
const value = index[key];
|
||||
if (Array.isArray(value)) {
|
||||
for (const idx of value) {
|
||||
const values = await this.db.getAllFromIndex(this.name, key, idx);
|
||||
result = new Set([...result, ...values]);
|
||||
}
|
||||
} else {
|
||||
const values = await this.db.getAllFromIndex(this.name, key, value);
|
||||
result = new Set([...result, ...values]);
|
||||
}
|
||||
}
|
||||
return Array.from(result);
|
||||
}
|
||||
|
||||
async #getAllByOffset(value: string, direction: 1 | -1, limit?: number) {
|
||||
if (direction === 1) {
|
||||
return this.db.getAll(this.name, IDBKeyRange.lowerBound(value), limit);
|
||||
@@ -233,33 +173,14 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async updateOne(
|
||||
filter: Filter<TSchema>,
|
||||
async update(
|
||||
condition: Criteria<TSchema>,
|
||||
modifier: Modifier<TSchema>,
|
||||
arrayFilters?: Filter<TSchema>[],
|
||||
condition?: Criteria<TSchema>,
|
||||
options: { cloneMode?: CloneMode; queryOptions?: Partial<Options> } = { cloneMode: "deep" },
|
||||
arrayFilters?: TSchema[],
|
||||
): Promise<UpdateResult> {
|
||||
if (typeof filter.id === "string") {
|
||||
return this.#update(filter.id, modifier, arrayFilters, condition, options);
|
||||
}
|
||||
const documents = await this.find(filter);
|
||||
if (documents.length > 0) {
|
||||
return this.#update(documents[0].id, modifier, arrayFilters, condition, options);
|
||||
}
|
||||
return new UpdateResult(0, 0);
|
||||
}
|
||||
const logger = new UpdateLog(this.name, { condition, modifier, arrayFilters });
|
||||
|
||||
async updateMany(
|
||||
filter: Filter<TSchema>,
|
||||
modifier: Modifier<TSchema>,
|
||||
arrayFilters?: Filter<TSchema>[],
|
||||
condition?: Criteria<TSchema>,
|
||||
options: { cloneMode?: CloneMode; queryOptions?: Partial<Options> } = { cloneMode: "deep" },
|
||||
): Promise<UpdateResult> {
|
||||
const logger = new UpdateLog(this.name, { filter, modifier, arrayFilters, condition, options });
|
||||
|
||||
const ids = await this.find(filter).then((data) => data.map((d) => d.id));
|
||||
const ids = await this.find(condition).then((data) => data.map((d) => d.id));
|
||||
|
||||
const documents: TSchema[] = [];
|
||||
let modifiedCount = 0;
|
||||
@@ -271,7 +192,7 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
if (current === undefined) {
|
||||
return;
|
||||
}
|
||||
const modified = update(current, modifier, arrayFilters, condition, options);
|
||||
const modified = update(current, modifier, arrayFilters, condition, { cloneMode: "deep" });
|
||||
if (modified.length > 0) {
|
||||
modifiedCount += 1;
|
||||
documents.push(current);
|
||||
@@ -283,71 +204,12 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
|
||||
await tx.done;
|
||||
|
||||
this.broadcast("updateMany", documents);
|
||||
this.broadcast("update", documents);
|
||||
this.#cache.flush();
|
||||
|
||||
this.log(logger.result());
|
||||
|
||||
return new UpdateResult(ids.length, modifiedCount);
|
||||
}
|
||||
|
||||
async replace(filter: Filter<TSchema>, document: TSchema): Promise<UpdateResult> {
|
||||
const logger = new ReplaceLog(this.name, document);
|
||||
|
||||
const ids = await this.find(filter).then((data) => data.map((d) => d.id));
|
||||
|
||||
const documents: TSchema[] = [];
|
||||
const count = ids.length;
|
||||
|
||||
const tx = this.db.transaction(this.name, "readwrite", { durability: "relaxed" });
|
||||
await Promise.all(
|
||||
ids.map((id) => {
|
||||
const next = { ...document, id };
|
||||
documents.push(next);
|
||||
return tx.store.put(next);
|
||||
}),
|
||||
);
|
||||
await tx.done;
|
||||
|
||||
this.broadcast("updateMany", documents);
|
||||
this.#cache.flush();
|
||||
|
||||
this.log(logger.result({ count }));
|
||||
|
||||
return new UpdateResult(count, count);
|
||||
}
|
||||
|
||||
async #update(
|
||||
id: string | number,
|
||||
modifier: Modifier<TSchema>,
|
||||
arrayFilters?: Filter<TSchema>[],
|
||||
condition?: Criteria<TSchema>,
|
||||
options: { cloneMode?: CloneMode; queryOptions?: Partial<Options> } = { cloneMode: "deep" },
|
||||
): Promise<UpdateResult> {
|
||||
const logger = new UpdateLog(this.name, { id, modifier });
|
||||
|
||||
const tx = this.db.transaction(this.name, "readwrite", { durability: "relaxed" });
|
||||
|
||||
const current = await tx.store.get(id);
|
||||
if (current === undefined) {
|
||||
await tx.done;
|
||||
return new UpdateResult(0, 0);
|
||||
}
|
||||
|
||||
const modified = await update(current, modifier, arrayFilters, condition, options);
|
||||
if (modified.length > 0) {
|
||||
await tx.store.put(current);
|
||||
}
|
||||
await tx.done;
|
||||
|
||||
if (modified.length > 0) {
|
||||
this.broadcast("updateOne", current);
|
||||
this.log(logger.result());
|
||||
this.#cache.flush();
|
||||
return new UpdateResult(1, 1);
|
||||
}
|
||||
|
||||
return new UpdateResult(1);
|
||||
return { matchedCount: ids.length, modifiedCount };
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -356,10 +218,10 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async remove(filter: Filter<TSchema>): Promise<RemoveResult> {
|
||||
const logger = new RemoveLog(this.name, { filter });
|
||||
async remove(condition: Criteria<TSchema>): Promise<number> {
|
||||
const logger = new RemoveLog(this.name, { condition });
|
||||
|
||||
const documents = await this.find(filter);
|
||||
const documents = await this.find(condition);
|
||||
const tx = this.db.transaction(this.name, "readwrite");
|
||||
|
||||
await Promise.all(documents.map((data) => tx.store.delete(data.id)));
|
||||
@@ -370,7 +232,7 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
|
||||
this.log(logger.result({ count: documents.length }));
|
||||
|
||||
return new RemoveResult(documents.length);
|
||||
return documents.length;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -379,9 +241,9 @@ export class IndexedDBStorage<TPrimaryKey extends string, TSchema extends Docume
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async count(filter?: Filter<TSchema>): Promise<number> {
|
||||
if (filter !== undefined) {
|
||||
return (await this.find(filter)).length;
|
||||
async count(condition: Criteria<TSchema>): Promise<number> {
|
||||
if (condition !== undefined) {
|
||||
return (await this.find(condition)).length;
|
||||
}
|
||||
return this.db.count(this.name);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
import { Collection } from "../../collection.ts";
|
||||
import type { Document } from "../../types.ts";
|
||||
import type { Registrars } from "../registrars.ts";
|
||||
import type { Index, Registrars } from "../../registrars.ts";
|
||||
import { MemoryStorage } from "./storage.ts";
|
||||
|
||||
type Options = {
|
||||
name: string;
|
||||
registrars: Registrars[];
|
||||
};
|
||||
export class MemoryDatabase<TOptions extends MemoryDatabaseOptions> {
|
||||
readonly #collections = new Map<string, Collection>();
|
||||
|
||||
export class MemoryDatabase<T extends Record<string, Document>> {
|
||||
readonly name: string;
|
||||
readonly #collections = new Map<keyof T, Collection<T[keyof T]>>();
|
||||
|
||||
constructor(readonly options: Options) {
|
||||
this.name = options.name;
|
||||
for (const { name } of options.registrars) {
|
||||
this.#collections.set(name, new Collection(name, new MemoryStorage(name)));
|
||||
constructor(readonly options: TOptions) {
|
||||
for (const { name, schema, indexes } of options.registrars) {
|
||||
this.#collections.set(
|
||||
name,
|
||||
new Collection({
|
||||
name,
|
||||
storage: new MemoryStorage(name, indexes),
|
||||
schema,
|
||||
indexes,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.options.name;
|
||||
}
|
||||
|
||||
get registrars() {
|
||||
return this.options.registrars;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Fetchers
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
collection<Name extends keyof T>(name: Name): Collection<T[Name]> {
|
||||
collection<
|
||||
TName extends TOptions["registrars"][number]["name"],
|
||||
TSchema = Extract<TOptions["registrars"][number], { name: TName }>["schema"],
|
||||
>(
|
||||
name: TName,
|
||||
): Collection<{
|
||||
name: TName;
|
||||
storage: MemoryStorage;
|
||||
schema: TSchema;
|
||||
indexes: Index[];
|
||||
}> {
|
||||
const collection = this.#collections.get(name);
|
||||
if (collection === undefined) {
|
||||
throw new Error(`Collection '${name as string}' not found`);
|
||||
@@ -45,3 +63,8 @@ export class MemoryDatabase<T extends Record<string, Document>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type MemoryDatabaseOptions<TRegistrars extends Array<Registrars> = Array<any>> = {
|
||||
name: string;
|
||||
registrars: TRegistrars;
|
||||
};
|
||||
|
||||
@@ -1,155 +1,88 @@
|
||||
import { Query, update } from "mingo";
|
||||
import type { AnyObject } from "mingo/types";
|
||||
import type { Criteria } from "mingo/types";
|
||||
import type { Modifier } from "mingo/updater";
|
||||
|
||||
import { getDocumentWithPrimaryKey } from "../../primary-key.ts";
|
||||
import { Collections } from "../../storage/collections.ts";
|
||||
import type { UpdatePayload } from "../../storage/mod.ts";
|
||||
import type { InsertResult } from "../../storage/operators/insert.ts";
|
||||
import type { UpdateResult } from "../../storage/operators/update.ts";
|
||||
import {
|
||||
addOptions,
|
||||
type CountPayload,
|
||||
type FindByIdPayload,
|
||||
type FindPayload,
|
||||
type InsertManyPayload,
|
||||
type InsertOnePayload,
|
||||
type RemovePayload,
|
||||
type ReplacePayload,
|
||||
Storage,
|
||||
} from "../../storage/storage.ts";
|
||||
import { IndexManager, type IndexSpec } from "../../index/manager.ts";
|
||||
import type { UpdateResult } from "../../storage.ts";
|
||||
import { addOptions, type QueryOptions, Storage } from "../../storage.ts";
|
||||
import type { AnyDocument } from "../../types.ts";
|
||||
|
||||
export class MemoryStorage extends Storage {
|
||||
readonly #collections = new Collections();
|
||||
export class MemoryStorage<TSchema extends AnyDocument = AnyDocument> extends Storage<TSchema> {
|
||||
readonly index: IndexManager<TSchema>;
|
||||
|
||||
constructor(name: string, indexes: IndexSpec[]) {
|
||||
super(name, indexes);
|
||||
this.index = new IndexManager(indexes);
|
||||
}
|
||||
|
||||
get documents() {
|
||||
return this.index.primary.tree;
|
||||
}
|
||||
|
||||
async resolve() {
|
||||
return this;
|
||||
}
|
||||
|
||||
async insertOne({ pkey, values, ...payload }: InsertOnePayload): Promise<InsertResult> {
|
||||
const collection = this.#collections.get(payload.collection);
|
||||
|
||||
const document = getDocumentWithPrimaryKey(pkey, values);
|
||||
if (collection.has(document[pkey])) {
|
||||
return { insertCount: 0, insertIds: [] };
|
||||
async insert(documents: TSchema[]): Promise<void> {
|
||||
for (const document of documents) {
|
||||
this.index.insert(document);
|
||||
}
|
||||
|
||||
collection.set(document[pkey], document);
|
||||
this.broadcast("insertOne", document);
|
||||
|
||||
return { insertCount: 1, insertIds: [document[pkey]] };
|
||||
this.broadcast("insert", documents);
|
||||
}
|
||||
|
||||
async insertMany({ pkey, values, ...payload }: InsertManyPayload): Promise<InsertResult> {
|
||||
const collection = this.#collections.get(payload.collection);
|
||||
|
||||
const documents: AnyDocument[] = [];
|
||||
for (const insert of values) {
|
||||
const document = getDocumentWithPrimaryKey(pkey, insert);
|
||||
if (collection.has(document[pkey])) {
|
||||
continue;
|
||||
}
|
||||
collection.set(document[pkey], document);
|
||||
documents.push(document);
|
||||
}
|
||||
|
||||
if (documents.length > 0) {
|
||||
this.broadcast("insertMany", documents);
|
||||
}
|
||||
|
||||
return { insertCount: documents.length, insertIds: documents.map((document) => document[pkey]) };
|
||||
async getByIndex(index: string, value: string): Promise<TSchema[]> {
|
||||
return this.index.get(index)?.get(value) ?? [];
|
||||
}
|
||||
|
||||
async findById({ collection, id }: FindByIdPayload): Promise<AnyObject | undefined> {
|
||||
return this.#collections.get(collection).get(id);
|
||||
}
|
||||
|
||||
async find({ condition = {}, options, ...payload }: FindPayload): Promise<AnyDocument[]> {
|
||||
let cursor = new Query(condition).find<AnyDocument>(this.#collections.documents(payload.collection));
|
||||
async find(condition: Criteria<TSchema> = {}, options?: QueryOptions): Promise<TSchema[]> {
|
||||
let cursor = new Query(condition).find<TSchema>(this.documents);
|
||||
if (options !== undefined) {
|
||||
cursor = addOptions(cursor, options);
|
||||
}
|
||||
return cursor.all();
|
||||
}
|
||||
|
||||
async updateOne({ pkey, condition, modifier, arrayFilters, ...payload }: UpdatePayload): Promise<UpdateResult> {
|
||||
const collection = this.#collections.get(payload.collection);
|
||||
async update(
|
||||
condition: Criteria<TSchema>,
|
||||
modifier: Modifier<TSchema>,
|
||||
arrayFilters?: TSchema[],
|
||||
): Promise<UpdateResult> {
|
||||
const documents: TSchema[] = [];
|
||||
|
||||
let matchedCount = 0;
|
||||
let modifiedCount = 0;
|
||||
|
||||
for (const document of await this.find({ collection: payload.collection, condition, options: { limit: 1 } })) {
|
||||
const modified = update(document, modifier, arrayFilters, undefined, { cloneMode: "deep" });
|
||||
if (modified.length > 0) {
|
||||
collection.set(document[pkey], document);
|
||||
this.broadcast("updateOne", document);
|
||||
modifiedCount += 1;
|
||||
}
|
||||
matchedCount += 1;
|
||||
}
|
||||
return { matchedCount, modifiedCount };
|
||||
}
|
||||
|
||||
async updateMany({ pkey, condition, modifier, arrayFilters, ...payload }: UpdatePayload): Promise<UpdateResult> {
|
||||
const collection = this.#collections.get(payload.collection);
|
||||
|
||||
const documents: AnyDocument[] = [];
|
||||
|
||||
let matchedCount = 0;
|
||||
let modifiedCount = 0;
|
||||
|
||||
for (const document of await this.find({ collection: payload.collection, condition })) {
|
||||
for (const document of await this.find(condition)) {
|
||||
matchedCount += 1;
|
||||
const modified = update(document, modifier, arrayFilters, undefined, { cloneMode: "deep" });
|
||||
if (modified.length > 0) {
|
||||
modifiedCount += 1;
|
||||
documents.push(document);
|
||||
collection.set(document[pkey], document);
|
||||
this.documents.add(document);
|
||||
}
|
||||
}
|
||||
|
||||
this.broadcast("updateMany", documents);
|
||||
|
||||
return { matchedCount, modifiedCount };
|
||||
}
|
||||
|
||||
async replace({ pkey, condition, document, ...payload }: ReplacePayload): Promise<UpdateResult> {
|
||||
const collection = this.#collections.get(payload.collection);
|
||||
|
||||
let matchedCount = 0;
|
||||
let modifiedCount = 0;
|
||||
|
||||
const documents: AnyDocument[] = [];
|
||||
for (const current of await this.find({ collection: payload.collection, condition })) {
|
||||
matchedCount += 1;
|
||||
modifiedCount += 1;
|
||||
documents.push(document);
|
||||
collection.set(current[pkey], document);
|
||||
if (modifiedCount > 0) {
|
||||
this.broadcast("update", documents);
|
||||
}
|
||||
|
||||
this.broadcast("updateMany", documents);
|
||||
|
||||
return { matchedCount, modifiedCount };
|
||||
}
|
||||
|
||||
async remove({ pkey, condition, ...payload }: RemovePayload): Promise<number> {
|
||||
const collection = this.#collections.get(payload.collection);
|
||||
|
||||
const documents = await this.find({ collection: payload.collection, condition });
|
||||
async remove(condition: Criteria<TSchema>): Promise<number> {
|
||||
const documents = await this.find(condition);
|
||||
for (const document of documents) {
|
||||
collection.delete(document[pkey]);
|
||||
this.documents.delete(document);
|
||||
}
|
||||
|
||||
this.broadcast("remove", documents);
|
||||
|
||||
return documents.length;
|
||||
}
|
||||
|
||||
async count({ collection, condition = {} }: CountPayload): Promise<number> {
|
||||
return new Query(condition).find(this.#collections.documents(collection)).all().length;
|
||||
async count(condition: Criteria<TSchema>): Promise<number> {
|
||||
return new Query(condition).find(this.documents).all().length;
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
this.#collections.flush();
|
||||
this.documents.clear();
|
||||
}
|
||||
}
|
||||
|
||||
29
src/databases/memory/tests/storage.test.ts
Normal file
29
src/databases/memory/tests/storage.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, it } from "@std/testing/bdd";
|
||||
import { expect } from "expect";
|
||||
|
||||
import { MemoryStorage } from "../storage.ts";
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Unit Tests
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
describe("Memory Storage", () => {
|
||||
it("should insert new records", async () => {
|
||||
const storage = new MemoryStorage("test", [["id", { primary: true }]]);
|
||||
|
||||
const documents = [
|
||||
{
|
||||
id: "abc",
|
||||
foo: "bar",
|
||||
},
|
||||
];
|
||||
|
||||
await storage.insert(documents);
|
||||
|
||||
console.log(storage);
|
||||
|
||||
expect(storage.documents).toContain(documents);
|
||||
});
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./indexeddb/database.ts";
|
||||
export * from "./memory/database.ts";
|
||||
@@ -1,147 +0,0 @@
|
||||
import { Query, update } from "mingo";
|
||||
import type { Criteria, Options } from "mingo/types";
|
||||
import type { CloneMode, Modifier } from "mingo/updater";
|
||||
|
||||
import { getDocumentWithPrimaryKey } from "../../primary-key.ts";
|
||||
import { DuplicateDocumentError } from "../../storage/errors.ts";
|
||||
import type { InsertResult } from "../../storage/operators/insert.ts";
|
||||
import { UpdateResult } from "../../storage/operators/update.ts";
|
||||
import { addOptions, type QueryOptions, Storage } from "../../storage/storage.ts";
|
||||
import type { AnyDocument } from "../../types.ts";
|
||||
|
||||
export class ObserverStorage extends Storage {
|
||||
readonly #documents = new Map<string, AnyDocument>();
|
||||
|
||||
async resolve() {
|
||||
return this;
|
||||
}
|
||||
|
||||
async has(id: string): Promise<boolean> {
|
||||
return this.#documents.has(id);
|
||||
}
|
||||
|
||||
async insertOne(values: AnyDocument): Promise<InsertResult> {
|
||||
const document = getDocumentWithPrimaryKey(this.primaryKey, values);
|
||||
if (await this.has(document[this.primaryKey])) {
|
||||
throw new DuplicateDocumentError(document, this as any);
|
||||
}
|
||||
this.#documents.set(document[this.primaryKey], document);
|
||||
return getInsertOneResult(document);
|
||||
}
|
||||
|
||||
async insertMany(list: TSchema[]): Promise<InsertResult> {
|
||||
const result: TSchema[] = [];
|
||||
for (const values of list) {
|
||||
const document = getDocumentWithPrimaryKey(this.primaryKey, values);
|
||||
result.push(document);
|
||||
this.#documents.set(document.id, document);
|
||||
}
|
||||
return getInsertManyResult(result);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<TSchema | undefined> {
|
||||
return this.#documents.get(id);
|
||||
}
|
||||
|
||||
async find(filter?: Filter<TSchema>, options?: QueryOptions): Promise<TSchema[]> {
|
||||
let cursor = new Query(filter ?? {}).find<TSchema>(Array.from(this.#documents.values()));
|
||||
if (options !== undefined) {
|
||||
cursor = addOptions(cursor, options);
|
||||
}
|
||||
return cursor.all();
|
||||
}
|
||||
|
||||
async updateOne(
|
||||
filter: Filter<TSchema>,
|
||||
modifier: Modifier<TSchema>,
|
||||
arrayFilters?: Filter<TSchema>[],
|
||||
condition?: Criteria<TSchema>,
|
||||
options: { cloneMode?: CloneMode; queryOptions?: Partial<Options> } = { cloneMode: "deep" },
|
||||
): Promise<UpdateResult> {
|
||||
const query = new Query(filter);
|
||||
for (const document of Array.from(this.#documents.values())) {
|
||||
if (query.test(document) === true) {
|
||||
const modified = update(document, modifier, arrayFilters, condition, options);
|
||||
if (modified.length > 0) {
|
||||
this.#documents.set(document.id, document);
|
||||
this.broadcast("updateOne", document);
|
||||
return new UpdateResult(1, 1);
|
||||
}
|
||||
return new UpdateResult(1, 0);
|
||||
}
|
||||
}
|
||||
return new UpdateResult(0, 0);
|
||||
}
|
||||
|
||||
async updateMany(
|
||||
filter: Filter<TSchema>,
|
||||
modifier: Modifier<TSchema>,
|
||||
arrayFilters?: Filter<TSchema>[],
|
||||
condition?: Criteria<TSchema>,
|
||||
options: { cloneMode?: CloneMode; queryOptions?: Partial<Options> } = { cloneMode: "deep" },
|
||||
): Promise<UpdateResult> {
|
||||
const query = new Query(filter);
|
||||
|
||||
const documents: TSchema[] = [];
|
||||
|
||||
let matchedCount = 0;
|
||||
let modifiedCount = 0;
|
||||
|
||||
for (const document of Array.from(this.#documents.values())) {
|
||||
if (query.test(document) === true) {
|
||||
matchedCount += 1;
|
||||
const modified = update(document, modifier, arrayFilters, condition, options);
|
||||
if (modified.length > 0) {
|
||||
modifiedCount += 1;
|
||||
documents.push(document);
|
||||
this.#documents.set(document.id, document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.broadcast("updateMany", documents);
|
||||
|
||||
return new UpdateResult(matchedCount, modifiedCount);
|
||||
}
|
||||
|
||||
async replace(filter: Filter<TSchema>, document: TSchema): Promise<UpdateResult> {
|
||||
const query = new Query(filter);
|
||||
|
||||
const documents: TSchema[] = [];
|
||||
|
||||
let matchedCount = 0;
|
||||
let modifiedCount = 0;
|
||||
|
||||
for (const current of Array.from(this.#documents.values())) {
|
||||
if (query.test(current) === true) {
|
||||
matchedCount += 1;
|
||||
modifiedCount += 1;
|
||||
documents.push(document);
|
||||
this.#documents.set(document.id, document);
|
||||
}
|
||||
}
|
||||
|
||||
return new UpdateResult(matchedCount, modifiedCount);
|
||||
}
|
||||
|
||||
async remove(filter: Filter<TSchema>): Promise<RemoveResult> {
|
||||
const documents = Array.from(this.#documents.values());
|
||||
const query = new Query(filter);
|
||||
let count = 0;
|
||||
for (const document of documents) {
|
||||
if (query.test(document) === true) {
|
||||
this.#documents.delete(document.id);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return new RemoveResult(count);
|
||||
}
|
||||
|
||||
async count(filter?: Filter<TSchema>): Promise<number> {
|
||||
return new Query(filter ?? {}).find(Array.from(this.#documents.values())).all().length;
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
this.#documents.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
export type Registrars = {
|
||||
/**
|
||||
* Name of the collection.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Set the primary key of the collection.
|
||||
* Default: "id"
|
||||
*/
|
||||
primaryKey?: string;
|
||||
|
||||
/**
|
||||
* List of custom indexes for the collection.
|
||||
*/
|
||||
indexes?: Index[];
|
||||
};
|
||||
|
||||
type Index = [IndexKey, IndexOptions?];
|
||||
|
||||
type IndexKey = string;
|
||||
|
||||
type IndexOptions = { unique: boolean };
|
||||
Reference in New Issue
Block a user