feat: 2.0.0 beta release

This commit is contained in:
2025-08-14 23:55:04 +02:00
parent 2eba1475c5
commit fab3476515
71 changed files with 2171 additions and 8936 deletions

View File

@@ -1,2 +0,0 @@
export * from "./IndexedDb.js";
export * from "./MemoryDb.js";

View File

@@ -1,3 +0,0 @@
export type { Action } from "./Action.js";
export * from "./Observe.js";
export * from "./ObserveOne.js";

View File

@@ -1 +0,0 @@
export * from "./Result.js";

View File

@@ -1 +0,0 @@
export * from "./Result.js";

View File

@@ -1,56 +0,0 @@
import * as dot from "dot-prop";
import { Document, Filter, UpdateFilter, WithId } from "../../../Types.js";
import { setPositionalData } from "./Utils.js";
/**
* Execute a $inc based operators.
*
* Supports positional array operator $(update)
*
* @see https://www.mongodb.com/docs/manual/reference/operator/update/positional
*
* @param document - Document being updated.
* @param filter - Search filter provided with the operation. Eg. updateOne({ id: "1" })
* @param $set - $set action being executed.
*/
export function $inc<TSchema extends Document = Document>(
document: WithId<TSchema>,
filter: Filter<WithId<TSchema>>,
$inc: UpdateFilter<TSchema>["$inc"] = {}
): boolean {
let modified = false;
for (const key in $inc) {
if (key.includes("$")) {
if (
setPositionalData(document, filter, key, {
object: (data, key, target) => {
if (typeof data === "number") {
return data + ($inc[key] as number);
}
const value = dot.getProperty(data, target);
if (typeof value !== "number") {
return 0;
}
return value + $inc[key];
},
value: (data, key) => data + $inc[key]
})
) {
modified = true;
}
} else {
document = increment(document, key, $inc[key]);
modified = true;
}
}
return modified;
}
function increment<D extends Document>(document: D, key: string, value: number): D {
let currentValue = dot.getProperty(document, key) as unknown;
if (typeof currentValue !== "number") {
currentValue = 0;
}
return dot.setProperty(document, key, (currentValue as number) + value);
}

View File

@@ -1,60 +0,0 @@
import * as dot from "dot-prop";
import { Query } from "mingo";
import { RawObject } from "mingo/types";
import { Document, UpdateFilter, WithId } from "../../../Types.js";
import { PullUpdateArrayError } from "../../Errors.js";
export function $pull<TSchema extends Document>(
document: WithId<TSchema>,
operator: UpdateFilter<TSchema>["$pull"] = {}
): boolean {
let modified = false;
for (const key in operator) {
const values = getPullValues(document, key);
const result = getPullResult(operator, key, values);
dot.setProperty(document, key, result);
if (result.length !== values.length) {
modified = true;
}
}
return modified;
}
function getPullValues(document: Document, key: string): any[] {
const values: any[] | undefined = dot.getProperty(document, key);
if (values === undefined || Array.isArray(values) === false) {
throw new PullUpdateArrayError(document.id, key);
}
return values;
}
function getPullResult(operator: RawObject, key: string, values: any[]): any[] {
if (typeof operator[key] === "object") {
return new Query(getPullCriteria(operator, key)).remove(values);
}
return values.filter((value) => value !== operator[key]);
}
/**
* Criteria used during pull depends on the structure of the query under the pulled
* key. If the object has a mongodb filter key with a $ prefix we need to provide
* the query with the array key as the query wrapper. If a $ prefix is not present
* we want the value under the key being the criteria.
*
* @param operator - Object under operator action.
* @param key - Specific key being resolved to a criteria.
*/
function getPullCriteria(operator: any, key: string): RawObject {
let hasFilters = false;
for (const left in operator[key]) {
if (left.includes("$")) {
hasFilters = true;
break;
}
}
if (hasFilters === true) {
return { [key]: dot.getProperty(operator, key) };
}
return dot.getProperty(operator, key) as RawObject;
}

View File

@@ -1,72 +0,0 @@
import * as dot from "dot-prop";
import { deepEqual } from "fast-equals";
import { Query } from "mingo";
import type { RawObject } from "mingo/types";
import { Document, UpdateFilter, WithId } from "../../../Types.js";
export function $push<TSchema extends Document = Document>(
document: WithId<TSchema>,
operator: UpdateFilter<TSchema>["$push"] = {}
): boolean {
let modified = false;
for (const key in operator) {
const values = getPushValues(document, key);
const result = getPushResult(operator, key, values);
dot.setProperty(document, key, result);
if (deepEqual(values, result) === false) {
modified = true;
}
}
return modified;
}
function getPushValues(document: any, key: string): any[] {
const values = dot.getProperty(document, key);
if (values === undefined) {
return [];
}
return values;
}
function getPushResult(operator: RawObject, key: string, values: any[]): any[] {
if (typeof operator[key] === "object") {
return getPushFromModifiers(operator[key], values);
}
return [...values, operator[key]];
}
function getPushFromModifiers(obj: any, values: any[]): any[] {
if (obj.$each === undefined) {
return [...values, obj];
}
let items: any[];
if (obj.$position !== undefined) {
items = [...values.slice(0, obj.$position), ...obj.$each, ...values.slice(obj.$position)];
} else {
items = [...values, ...obj.$each];
}
if (obj.$sort !== undefined) {
if (typeof obj.$sort === "object") {
items = new Query({}).find(items).sort(obj.$sort).all();
} else {
items = items.sort((a, b) => {
if (obj.$sort === 1) {
return a < b ? -1 : 1;
}
return a < b ? 1 : -1;
});
}
}
if (obj.$slice !== undefined) {
if (obj.$slice < 0) {
return items.slice(obj.$slice);
}
return items.slice(0, obj.$slice);
}
return items;
}

View File

@@ -1,3 +0,0 @@
export class UpdateResult {
constructor(readonly matched = 0, readonly modified = 0) {}
}

View File

@@ -1,47 +0,0 @@
import * as dot from "dot-prop";
import { Document, Filter, UpdateFilter, WithId } from "../../../Types.js";
import { setPositionalData } from "./Utils.js";
/**
* Execute a $set based operators.
*
* Supports positional array operator $(update)
*
* @see https://www.mongodb.com/docs/manual/reference/operator/update/positional
*
* @param document - Document being updated.
* @param filter - Search filter provided with the operation. Eg. updateOne({ id: "1" })
* @param $set - $set action being executed.
*/
export function $set<TSchema extends Document = Document>(
document: WithId<WithId<TSchema>>,
filter: Filter<WithId<TSchema>>,
$set: UpdateFilter<TSchema>["$set"] = {} as any
): boolean {
let modified = false;
for (const key in $set) {
if (key.includes("$")) {
if (
setPositionalData(document, filter, key, {
object: (data, key) => getSetValue(data, key, $set),
value: (_, key) => $set[key]
})
) {
modified = true;
}
} else {
document = dot.setProperty(document, key, getSetValue(document, key, $set));
modified = true;
}
}
return modified;
}
function getSetValue(data: any, key: string, $set: UpdateFilter<Document>["$set"] = {}) {
const value = $set[key];
if (typeof value === "function") {
return value(dot.getProperty(data, key), data);
}
return value;
}

View File

@@ -1,16 +0,0 @@
import * as dot from "dot-prop";
import { Document, UpdateFilter, WithId } from "../../../Types.js";
export function $unset<TSchema extends Document = Document>(
document: WithId<TSchema>,
$unset: UpdateFilter<TSchema>["$unset"] = {}
): boolean {
let modified = false;
for (const key in $unset) {
if (dot.deleteProperty(document, key)) {
modified = true;
}
}
return modified;
}

View File

@@ -1,26 +0,0 @@
import { clone } from "../../../Clone.js";
import { Document, Filter, UpdateFilter, WithId } from "../../../Types.js";
import { $inc } from "./Inc.js";
import { $pull } from "./Pull.js";
import { $push } from "./Push.js";
import { $set } from "./Set.js";
import { $unset } from "./Unset.js";
export function update<TSchema extends Document>(
filter: Filter<WithId<TSchema>>,
operators: UpdateFilter<TSchema>,
document: WithId<TSchema>
) {
const updatedDocument = clone(document);
const setModified = $set<TSchema>(updatedDocument, filter, operators.$set);
const runModified = $unset<TSchema>(updatedDocument, operators.$unset);
const pushModified = $push<TSchema>(updatedDocument, operators.$push);
const pullModified = $pull<TSchema>(updatedDocument, operators.$pull);
const incModified = $inc<TSchema>(updatedDocument, filter, operators.$inc);
return {
modified: setModified || runModified || pushModified || pullModified || incModified,
document: updatedDocument
};
}

View File

@@ -1,168 +0,0 @@
import * as dot from "dot-prop";
import { deepEqual } from "fast-equals";
import { Query } from "mingo";
import { clone } from "../../../Clone.js";
import { Document, Filter, WithId } from "../../../Types.js";
type UpdateValue = (data: any, key: string, target: string) => any;
export function setPositionalData<TSchema extends Document = Document>(
document: WithId<TSchema>,
criteria: Filter<WithId<TSchema>>,
key: string,
update: {
object: UpdateValue;
value: UpdateValue;
}
): boolean {
const { filter, path, target } = getPositionalFilter(criteria, key);
const values = getPropertyValues(document, path);
const items =
typeof filter === "object"
? getPositionalUpdateQuery(clone(values), key, filter, target, update.object)
: getPositionalUpdate(clone(values), key, filter, target, update.value);
dot.setProperty(document, path, items);
return deepEqual(values, items) === false;
}
function getPropertyValues(document: Document, path: string): string[] {
const values = dot.getProperty(document, path);
if (values === undefined) {
throw new Error("Values is undefined");
}
if (Array.isArray(values) === false) {
throw new Error("Values is not an array");
}
return values;
}
export function getPositionalUpdate(
items: any[],
key: string,
filter: string,
target: string,
updateValue: UpdateValue
): any[] {
let index = 0;
for (const item of items) {
if (item === filter) {
items[index] = updateValue(items[index], key, target);
break;
}
index += 1;
}
return items;
}
export function getPositionalUpdateQuery(
items: any[],
key: string,
filter: Filter<any>,
target: string,
updateValue: UpdateValue
): any[] {
let index = 0;
for (const item of items) {
if (new Query(filter).test(item) === true) {
if (target === "") {
items[index] = updateValue(items[index], key, target);
} else {
dot.setProperty(item, target, updateValue(items[index], key, target));
}
break;
}
index += 1;
}
return items;
}
export function getPositionalFilter(criteria: Filter<any>, key: string): PositionalFilter {
const [leftPath, rightPath] = key.split("$");
const lKey = trimSeparators(leftPath);
const rKey = trimSeparators(rightPath);
for (const key in criteria) {
const result = getPositionalCriteriaFilter(key, lKey, rKey, criteria);
if (result !== undefined) {
return result;
}
}
return {
filter: criteria[lKey],
path: lKey,
target: rKey
};
}
function getPositionalCriteriaFilter(
key: string,
lKey: string,
rKey: string,
criteria: Filter<any>
): PositionalFilter | undefined {
if (key.includes(lKey) === true) {
const isObject = typeof criteria[key] === "object";
if (key.includes(".") === true || isObject === true) {
return {
filter:
trimSeparators(key.replace(lKey, "")) === ""
? (criteria[key] as any).$elemMatch !== undefined
? (criteria[key] as any).$elemMatch
: criteria[key]
: {
[trimSeparators(key.replace(lKey, ""))]: criteria[key]
},
path: lKey,
target: rKey
};
}
}
return undefined;
}
function trimSeparators(value: string): string {
return value.replace(/^\.+|\.+$/gm, "");
}
/**
* A position filter is used to find documents to update in an array of values.
*
* @example
*
* ```ts
* const document = {
* grades: [
* { grade: 80, mean: 75, std: 8 },
* { grade: 85, mean: 90, std: 5 },
* { grade: 85, mean: 85, std: 8 }
* ]
* }
*
* updateOne({ "grades.grade": 85 }, { $set: { "grades.$.std": 6 } } })
* ```
*
* In the above example the filter would be `{ grade: 85 }` which is used to find
* objects to update in an array of values.
*/
type PositionalFilter = {
/**
* The filter to use to find the values to update in an array.
*/
filter: any;
/**
* The path to the array of values of the parent document. Eg. `grades`.
*/
path: string;
/**
* The path to the key to update in the array of values. Eg. `std`.
*/
target: string;
};

View File

@@ -1,2 +0,0 @@
export * from "./Result.js";
export * from "./Update.js";

View File

@@ -1,5 +0,0 @@
export * from "./Errors.js";
export * from "./Operators/Insert/mod.js";
export * from "./Operators/Remove/mod.js";
export * from "./Operators/Update/mod.js";
export * from "./Storage.js";

View File

@@ -1,4 +1,4 @@
import { Document, WithId } from "./Types.js";
import type { Document, WithId } from "./types.ts";
export const BroadcastChannel =
globalThis.BroadcastChannel ??

View File

@@ -1,6 +1,8 @@
import { UpdateOptions } from "mingo/core";
import { UpdateExpression } from "mingo/updater";
import { Observable, Subscription } from "rxjs";
import { observe, observeOne } from "./Observe/mod.js";
import { observe, observeOne } from "./observe/mod.ts";
import {
ChangeEvent,
InsertManyResult,
@@ -8,9 +10,9 @@ import {
Options,
RemoveResult,
Storage,
UpdateResult
} from "./Storage/mod.js";
import { Document, Filter, UpdateFilter, WithId } from "./Types.js";
UpdateResult,
} from "./storage/mod.ts";
import { Document, Filter, WithId } from "./types.ts";
/*
|--------------------------------------------------------------------------------
@@ -21,7 +23,7 @@ import { Document, Filter, UpdateFilter, WithId } from "./Types.js";
export class Collection<TSchema extends Document = Document> {
constructor(
readonly name: string,
readonly storage: Storage<TSchema>
readonly storage: Storage<TSchema>,
) {}
get observable() {
@@ -42,12 +44,24 @@ export class Collection<TSchema extends Document = Document> {
return this.storage.resolve().then((storage) => storage.insertMany(documents));
}
async updateOne(filter: Filter<WithId<TSchema>>, update: UpdateFilter<TSchema>): Promise<UpdateResult> {
return this.storage.resolve().then((storage) => storage.updateOne(filter, update));
async updateOne(
filter: Filter<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): Promise<UpdateResult> {
return this.storage.resolve().then((storage) => storage.updateOne(filter, expr, arrayFilters, condition, options));
}
async updateMany(filter: Filter<WithId<TSchema>>, update: UpdateFilter<TSchema>): Promise<UpdateResult> {
return this.storage.resolve().then((storage) => storage.updateMany(filter, update));
async updateMany(
filter: Filter<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): Promise<UpdateResult> {
return this.storage.resolve().then((storage) => storage.updateMany(filter, expr, arrayFilters, condition, options));
}
async replaceOne(filter: Filter<WithId<TSchema>>, document: TSchema): Promise<UpdateResult> {
@@ -67,29 +81,29 @@ export class Collection<TSchema extends Document = Document> {
subscribe(
filter?: Filter<WithId<TSchema>>,
options?: SubscribeToSingle,
next?: (document: WithId<TSchema> | undefined) => void
next?: (document: WithId<TSchema> | undefined) => void,
): Subscription;
subscribe(
filter?: Filter<WithId<TSchema>>,
options?: SubscribeToMany,
next?: (documents: WithId<TSchema>[], changed: WithId<TSchema>[], type: ChangeEvent["type"]) => void
next?: (documents: WithId<TSchema>[], changed: WithId<TSchema>[], type: ChangeEvent["type"]) => void,
): Subscription;
subscribe(filter: Filter<WithId<TSchema>> = {}, options?: Options, next?: (...args: any[]) => void): Subscription {
if (options?.limit === 1) {
return this.#observeOne(filter).subscribe({ next });
}
return this.#observe(filter, options).subscribe({
next: (value: [WithId<TSchema>[], WithId<TSchema>[], ChangeEvent["type"]]) => next?.(...value)
next: (value: [WithId<TSchema>[], WithId<TSchema>[], ChangeEvent["type"]]) => next?.(...value),
});
}
#observe(
filter: Filter<WithId<TSchema>> = {},
options?: Options
options?: Options,
): Observable<[WithId<TSchema>[], WithId<TSchema>[], ChangeEvent["type"]]> {
return new Observable<[WithId<TSchema>[], WithId<TSchema>[], ChangeEvent["type"]]>((subscriber) => {
return observe(this as any, filter, options, (values, changed, type) =>
subscriber.next([values, changed, type] as any)
subscriber.next([values, changed, type] as any),
);
});
}

View File

@@ -1,25 +1,11 @@
import type { IDBPDatabase } from "idb";
import { Query } from "mingo";
import type { AnyVal } from "mingo/types";
import { nanoid } from "nanoid";
import { DBLogger, InsertLog, QueryLog, RemoveLog, ReplaceLog, UpdateLog } from "../Logger.js";
import {
addOptions,
DuplicateDocumentError,
getInsertManyResult,
getInsertOneResult,
Index,
InsertManyResult,
InsertOneResult,
Options,
RemoveResult,
Storage,
update,
UpdateResult
} from "../Storage/mod.js";
import { Document, Filter, UpdateFilter, WithId } from "../Types.js";
import { IndexedDbCache } from "./IndexedDb.Cache.js";
import { DBLogger, InsertLog, QueryLog, RemoveLog, ReplaceLog, UpdateLog } from "../../logger.ts";
import { Storage } from "../../storage/storage.ts";
import { Document, Filter, UpdateFilter, WithId } from "../../types.ts";
import { IndexedDbCache } from "./cache.ts";
const OBJECT_PROTOTYPE = Object.getPrototypeOf({}) as AnyVal;
const OBJECT_TAG = "[object Object]";
@@ -33,7 +19,7 @@ export class IndexedDbStorage<TSchema extends Document = Document> extends Stora
constructor(
name: string,
promise: Promise<IDBPDatabase>,
readonly log: DBLogger
readonly log: DBLogger,
) {
super(name);
this.#promise = promise;
@@ -70,7 +56,7 @@ export class IndexedDbStorage<TSchema extends Document = Document> extends Stora
async insertOne(data: Partial<WithId<TSchema>>): Promise<InsertOneResult> {
const logger = new InsertLog(this.name);
const document = { ...data, id: data.id ?? nanoid() } as any;
const document = { ...data, id: data.id ?? crypto.randomUUID() } as any;
if (await this.has(document.id)) {
throw new DuplicateDocumentError(document, this as any);
}
@@ -95,7 +81,7 @@ export class IndexedDbStorage<TSchema extends Document = Document> extends Stora
const document = { ...data, id: data.id ?? nanoid() } as WithId<TSchema>;
documents.push(document);
return tx.store.add(document);
})
}),
);
await tx.done;
@@ -262,8 +248,8 @@ export class IndexedDbStorage<TSchema extends Document = Document> extends Stora
documents.push(document);
return tx.store.put(document);
}
})
)
}),
),
);
await tx.done;
@@ -290,7 +276,7 @@ export class IndexedDbStorage<TSchema extends Document = Document> extends Stora
const next = { ...document, id };
documents.push(next);
return tx.store.put(next);
})
}),
);
await tx.done;
@@ -305,7 +291,7 @@ export class IndexedDbStorage<TSchema extends Document = Document> extends Stora
async #update(
id: string | number,
filter: Filter<WithId<TSchema>>,
operators: UpdateFilter<TSchema>
operators: UpdateFilter<TSchema>,
): Promise<UpdateResult> {
const logger = new UpdateLog(this.name, { filter, operators });

View File

@@ -1,6 +1,6 @@
import { hashCodeQuery } from "../Hash.js";
import { Options } from "../Storage/mod.js";
import type { Document, Filter, WithId } from "../Types.js";
import { hashCodeQuery } from "../../hash.ts";
import { Options } from "../../storage/mod.ts";
import type { Document, Filter, WithId } from "../../types.ts";
export class IndexedDbCache<TSchema extends Document = Document> {
readonly #cache = new Map<number, string[]>();
@@ -13,7 +13,7 @@ export class IndexedDbCache<TSchema extends Document = Document> {
set(hashCode: number, documents: WithId<TSchema>[]) {
this.#cache.set(
hashCode,
documents.map((document) => document.id)
documents.map((document) => document.id),
);
for (const document of documents) {
this.#documents.set(document.id, document);

View File

@@ -1,10 +1,10 @@
import { IDBPDatabase, openDB } from "idb/with-async-ittr";
import { IDBPDatabase, openDB } from "idb";
import { Collection } from "../Collection.js";
import { DBLogger } from "../Logger.js";
import { Document } from "../Types.js";
import { IndexedDbStorage } from "./IndexedDb.Storage.js";
import { Registrars } from "./Registrars.js";
import { Collection } from "../../collection.ts";
import { DBLogger } from "../../logger.ts";
import { Document } from "../../types.ts";
import { Registrars } from "../registrars.ts";
import { IndexedDbStorage } from "./storage.ts";
function log() {}
@@ -31,7 +31,7 @@ export class IndexedDatabase<T extends StringRecord<Document>> {
store.createIndex(keyPath, keyPath, options);
}
}
}
},
});
for (const { name } of options.registrars) {
this.#collections.set(name, new Collection(name, new IndexedDbStorage(name, this.#db, options.log ?? log)));

View File

@@ -0,0 +1,288 @@
import { IDBPDatabase } from "idb";
import { createUpdater, Query } from "mingo";
import { UpdateOptions } from "mingo/core";
import { UpdateExpression } from "mingo/updater";
import { DBLogger, InsertLog, QueryLog, RemoveLog, ReplaceLog, UpdateLog } from "../../logger.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, Options, Storage } from "../../storage/storage.ts";
import type { Document, Filter, WithId } from "../../types.ts";
import { IndexedDbCache } from "./cache.ts";
const update = createUpdater({ cloneMode: "deep" });
export class IndexedDbStorage<TSchema extends Document = Document> extends Storage<TSchema> {
readonly #cache = new IndexedDbCache<TSchema>();
readonly #documents = new Map<string, WithId<TSchema>>();
readonly #promise: Promise<IDBPDatabase>;
#db?: IDBPDatabase;
constructor(
name: string,
promise: Promise<IDBPDatabase>,
readonly log: DBLogger,
) {
super(name);
this.#promise = promise;
}
async resolve() {
if (this.#db === undefined) {
this.#db = await this.#promise;
}
const documents = await this.db.getAll(this.name);
for (const document of documents) {
this.#documents.set(document.id, document);
}
return this;
}
async has(id: string): Promise<boolean> {
return this.#documents.has(id);
}
get db() {
if (this.#db === undefined) {
throw new Error("Database not initialized");
}
return this.#db;
}
/*
|--------------------------------------------------------------------------------
| Insert
|--------------------------------------------------------------------------------
*/
async insertOne(data: Partial<TSchema>): Promise<InsertOneResult> {
const logger = new InsertLog(this.name);
const document = { ...data, id: data.id ?? crypto.randomUUID() } as WithId<TSchema>;
if (await this.has(document.id)) {
throw new DuplicateDocumentError(document, this as any);
}
this.#documents.set(document.id, document);
this.broadcast("insertOne", document);
this.#cache.flush();
this.log(logger.result());
return getInsertOneResult(document);
}
async insertMany(documents: Partial<TSchema>[]): Promise<InsertManyResult> {
const logger = new InsertLog(this.name);
const result: TSchema[] = [];
for (const data of documents) {
const document = { ...data, id: data.id ?? crypto.randomUUID() } as WithId<TSchema>;
result.push(document);
this.#documents.set(document.id, document);
}
this.broadcast("insertMany", result);
this.#cache.flush();
this.log(logger.result());
return getInsertManyResult(result);
}
/*
|--------------------------------------------------------------------------------
| Read
|--------------------------------------------------------------------------------
*/
async findById(id: string): Promise<WithId<TSchema> | undefined> {
return this.#documents.get(id);
}
async find(filter: Filter<WithId<TSchema>>, options: Options = {}): Promise<WithId<TSchema>[]> {
const logger = new QueryLog(this.name, { filter, options });
const hashCode = this.#cache.hash(filter, options);
const cached = this.#cache.get(hashCode);
if (cached !== undefined) {
this.log(logger.result({ cached: true }));
return cached;
}
let cursor = new Query(filter ?? {}).find<TSchema>(Array.from(this.#documents.values()));
if (options !== undefined) {
cursor = addOptions(cursor, options);
}
return cursor.all() as WithId<TSchema>[];
}
/*
|--------------------------------------------------------------------------------
| Update
|--------------------------------------------------------------------------------
*/
async updateOne(
filter: Filter<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): 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, expr, 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<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): Promise<UpdateResult> {
const logger = new UpdateLog(this.name, { filter, expr, arrayFilters, condition, options });
const query = new Query(filter);
const documents: WithId<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, expr, arrayFilters, condition, options);
if (modified.length > 0) {
modifiedCount += 1;
documents.push(document);
this.#documents.set(document.id, document);
}
}
}
this.broadcast("updateMany", documents);
this.#cache.flush();
this.log(logger.result());
return new UpdateResult(matchedCount, modifiedCount);
}
async replace(filter: Filter<WithId<TSchema>>, document: WithId<TSchema>): Promise<UpdateResult> {
const logger = new ReplaceLog(this.name, document);
const query = new Query(filter);
const documents: WithId<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);
}
}
this.broadcast("updateMany", documents);
this.#cache.flush();
this.log(logger.result({ count: matchedCount }));
return new UpdateResult(matchedCount, modifiedCount);
}
/*
|--------------------------------------------------------------------------------
| Remove
|--------------------------------------------------------------------------------
*/
async remove(filter: Filter<WithId<TSchema>>): Promise<RemoveResult> {
const logger = new RemoveLog(this.name, { filter });
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);
this.broadcast("remove", document);
count += 1;
}
}
this.#cache.flush();
this.log(logger.result({ count: documents.length }));
return new RemoveResult(count);
}
/*
|--------------------------------------------------------------------------------
| Count
|--------------------------------------------------------------------------------
*/
async count(filter?: Filter<WithId<TSchema>>): Promise<number> {
return new Query(filter ?? {}).find(Array.from(this.#documents.values())).count();
}
/*
|--------------------------------------------------------------------------------
| Flush
|--------------------------------------------------------------------------------
*/
async flush(): Promise<void> {
await this.db.clear(this.name);
this.#documents.clear();
}
/*
|--------------------------------------------------------------------------------
| Save
|--------------------------------------------------------------------------------
*/
async save(): Promise<void> {
// this.db.
}
}
/*
const logger = new InsertLog(this.name);
const document = { ...data, id: data.id ?? crypto.randomUUID() } as any;
if (await this.has(document.id)) {
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);
*/

View File

@@ -1,7 +1,7 @@
import { Collection } from "../Collection.js";
import { Document } from "../Types.js";
import { MemoryStorage } from "./MemoryDb.Storage.js";
import { Registrars } from "./Registrars.js";
import { Collection } from "../../collection.ts";
import { Document } from "../../types.ts";
import { Registrars } from "../registrars.ts";
import { MemoryStorage } from "./storage.ts";
type Options = {
name: string;

View File

@@ -1,20 +1,20 @@
import { Query } from "mingo";
import { nanoid } from "nanoid";
import { createUpdater, Query } from "mingo";
import { UpdateOptions } from "mingo/core";
import { UpdateExpression } from "mingo/updater";
import { DuplicateDocumentError } from "../../storage/errors.ts";
import {
addOptions,
DuplicateDocumentError,
getInsertManyResult,
getInsertOneResult,
InsertManyResult,
InsertOneResult,
Options,
RemoveResult,
Storage,
update,
UpdateResult
} from "../Storage/mod.js";
import { Document, Filter, UpdateFilter, WithId } from "../Types.js";
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, Options, Storage } from "../../storage/storage.ts";
import type { Document, Filter, WithId } from "../../types.ts";
const update = createUpdater({ cloneMode: "deep" });
export class MemoryStorage<TSchema extends Document = Document> extends Storage<TSchema> {
readonly #documents = new Map<string, WithId<TSchema>>();
@@ -28,7 +28,7 @@ export class MemoryStorage<TSchema extends Document = Document> extends Storage<
}
async insertOne(data: Partial<TSchema>): Promise<InsertOneResult> {
const document = { ...data, id: data.id ?? nanoid() } as WithId<TSchema>;
const document = { ...data, id: data.id ?? crypto.randomUUID() } as WithId<TSchema>;
if (await this.has(document.id)) {
throw new DuplicateDocumentError(document, this as any);
}
@@ -40,7 +40,7 @@ export class MemoryStorage<TSchema extends Document = Document> extends Storage<
async insertMany(documents: Partial<TSchema>[]): Promise<InsertManyResult> {
const result: TSchema[] = [];
for (const data of documents) {
const document = { ...data, id: data.id ?? nanoid() } as WithId<TSchema>;
const document = { ...data, id: data.id ?? crypto.randomUUID() } as WithId<TSchema>;
result.push(document);
this.#documents.set(document.id, document);
}
@@ -62,12 +62,18 @@ export class MemoryStorage<TSchema extends Document = Document> extends Storage<
return cursor.all() as WithId<TSchema>[];
}
async updateOne(filter: Filter<WithId<TSchema>>, operators: UpdateFilter<TSchema>): Promise<UpdateResult> {
async updateOne(
filter: Filter<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): Promise<UpdateResult> {
const query = new Query(filter);
for (const current of Array.from(this.#documents.values())) {
if (query.test(current) === true) {
const { modified, document } = update<TSchema>(filter, operators, current);
if (modified === true) {
for (const document of Array.from(this.#documents.values())) {
if (query.test(document) === true) {
const modified = update(document, expr, arrayFilters, condition, options);
if (modified.length > 0) {
this.#documents.set(document.id, document);
this.broadcast("updateOne", document);
return new UpdateResult(1, 1);
@@ -78,7 +84,13 @@ export class MemoryStorage<TSchema extends Document = Document> extends Storage<
return new UpdateResult(0, 0);
}
async updateMany(filter: Filter<WithId<TSchema>>, operators: UpdateFilter<TSchema>): Promise<UpdateResult> {
async updateMany(
filter: Filter<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): Promise<UpdateResult> {
const query = new Query(filter);
const documents: WithId<TSchema>[] = [];
@@ -86,11 +98,11 @@ export class MemoryStorage<TSchema extends Document = Document> extends Storage<
let matchedCount = 0;
let modifiedCount = 0;
for (const current of Array.from(this.#documents.values())) {
if (query.test(current) === true) {
for (const document of Array.from(this.#documents.values())) {
if (query.test(document) === true) {
matchedCount += 1;
const { modified, document } = update<TSchema>(filter, operators, current);
if (modified === true) {
const modified = update(document, expr, arrayFilters, condition, options);
if (modified.length > 0) {
modifiedCount += 1;
documents.push(document);
this.#documents.set(document.id, document);

2
src/databases/mod.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./indexeddb/database.ts";
export * from "./memory/database.ts";

View File

@@ -1,20 +1,20 @@
import { Query } from "mingo";
import { nanoid } from "nanoid";
import { createUpdater, Query } from "mingo";
import { UpdateOptions } from "mingo/core";
import { UpdateExpression } from "mingo/updater";
import { DuplicateDocumentError } from "../../storage/errors.ts";
import {
addOptions,
DuplicateDocumentError,
getInsertManyResult,
getInsertOneResult,
InsertManyResult,
InsertOneResult,
Options,
RemoveResult,
Storage,
update,
UpdateResult
} from "../Storage/mod.js";
import { Document, Filter, UpdateFilter, WithId } from "../Types.js";
} from "../../storage/operators/insert.ts";
import { RemoveResult } from "../../storage/operators/remove.ts";
import { UpdateResult } from "../../storage/operators/update.ts";
import { addOptions, Options, Storage } from "../../storage/storage.ts";
import { Document, Filter, WithId } from "../../types.ts";
const update = createUpdater({ cloneMode: "deep" });
export class ObserverStorage<TSchema extends Document = Document> extends Storage<TSchema> {
readonly #documents = new Map<string, WithId<TSchema>>();
@@ -28,7 +28,7 @@ export class ObserverStorage<TSchema extends Document = Document> extends Storag
}
async insertOne(data: Partial<TSchema>): Promise<InsertOneResult> {
const document = { ...data, id: data.id ?? nanoid() } as WithId<TSchema>;
const document = { ...data, id: data.id ?? crypto.randomUUID() } as WithId<TSchema>;
if (await this.has(document.id)) {
throw new DuplicateDocumentError(document, this as any);
}
@@ -39,7 +39,7 @@ export class ObserverStorage<TSchema extends Document = Document> extends Storag
async insertMany(documents: Partial<TSchema>[]): Promise<InsertManyResult> {
const result: TSchema[] = [];
for (const data of documents) {
const document = { ...data, id: data.id ?? nanoid() } as WithId<TSchema>;
const document = { ...data, id: data.id ?? crypto.randomUUID() } as WithId<TSchema>;
result.push(document);
this.#documents.set(document.id, document);
}
@@ -58,13 +58,20 @@ export class ObserverStorage<TSchema extends Document = Document> extends Storag
return cursor.all() as WithId<TSchema>[];
}
async updateOne(filter: Filter<WithId<TSchema>>, operators: UpdateFilter<TSchema>): Promise<UpdateResult> {
async updateOne(
filter: Filter<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): Promise<UpdateResult> {
const query = new Query(filter);
for (const current of Array.from(this.#documents.values())) {
if (query.test(current) === true) {
const { modified, document } = update<TSchema>(filter, operators, current);
if (modified === true) {
for (const document of Array.from(this.#documents.values())) {
if (query.test(document) === true) {
const modified = update(document, expr, 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);
@@ -73,7 +80,13 @@ export class ObserverStorage<TSchema extends Document = Document> extends Storag
return new UpdateResult(0, 0);
}
async updateMany(filter: Filter<WithId<TSchema>>, operators: UpdateFilter<TSchema>): Promise<UpdateResult> {
async updateMany(
filter: Filter<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): Promise<UpdateResult> {
const query = new Query(filter);
const documents: WithId<TSchema>[] = [];
@@ -81,11 +94,11 @@ export class ObserverStorage<TSchema extends Document = Document> extends Storag
let matchedCount = 0;
let modifiedCount = 0;
for (const current of Array.from(this.#documents.values())) {
if (query.test(current) === true) {
for (const document of Array.from(this.#documents.values())) {
if (query.test(document) === true) {
matchedCount += 1;
const { modified, document } = update<TSchema>(filter, operators, current);
if (modified === true) {
const modified = update(filter, expr, arrayFilters, condition, options);
if (modified.length > 0) {
modifiedCount += 1;
documents.push(document);
this.#documents.set(document.id, document);
@@ -93,6 +106,8 @@ export class ObserverStorage<TSchema extends Document = Document> extends Storag
}
}
this.broadcast("updateMany", documents);
return new UpdateResult(matchedCount, modifiedCount);
}

View File

@@ -3,4 +3,4 @@ export type Registrars = {
indexes?: Index[];
};
type Index = [string, IDBIndexParameters?];
type Index = [string, any?];

View File

@@ -1,4 +0,0 @@
export * from "./Collection.js";
export * from "./Databases/mod.js";
export * from "./Storage/mod.js";
export type { Document, Filter } from "./Types.js";

View File

@@ -14,7 +14,10 @@ abstract class LogEvent {
data?: Record<string, any>;
constructor(readonly collection: string, readonly query?: Record<string, any>) {}
constructor(
readonly collection: string,
readonly query?: Record<string, any>,
) {}
result(data?: Record<string, any>): this {
this.performance.result();

4
src/mod.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from "./collection.ts";
export * from "./databases/mod.ts";
export * from "./storage/mod.ts";
export type { Document, Filter } from "./types.ts";

View File

@@ -1,10 +1,10 @@
import { Query } from "mingo";
import { Document, Filter, WithId } from "../Types.js";
import type { Document, Filter, WithId } from "../types.ts";
export function isMatch<TSchema extends Document = Document>(
document: WithId<TSchema>,
filter?: Filter<WithId<TSchema>>
filter?: Filter<WithId<TSchema>>,
): boolean {
return !filter || new Query(filter).test(document);
}

3
src/observe/mod.ts Normal file
View File

@@ -0,0 +1,3 @@
export type { Action } from "./action.ts";
export * from "./observe.ts";
export * from "./observe-one.ts";

View File

@@ -1,11 +1,11 @@
import { Collection } from "../Collection.js";
import { Document, Filter, WithId } from "../Types.js";
import { isMatch } from "./IsMatch.js";
import { Collection } from "../collection.ts";
import { Document, Filter, WithId } from "../types.ts";
import { isMatch } from "./is-match.ts";
export function observeOne<TSchema extends Document = Document>(
collection: Collection<TSchema>,
filter: Filter<WithId<TSchema>>,
onChange: (document: Document | undefined) => void
onChange: (document: Document | undefined) => void,
): {
unsubscribe: () => void;
} {
@@ -35,6 +35,6 @@ export function observeOne<TSchema extends Document = Document>(
return {
unsubscribe: () => {
subscription.unsubscribe();
}
},
};
}

View File

@@ -1,21 +1,21 @@
import { Query } from "mingo";
import { Collection } from "../Collection.js";
import { addOptions, ChangeEvent, Options } from "../Storage/mod.js";
import { Document, Filter, WithId } from "../Types.js";
import { Store } from "./Store.js";
import { Collection } from "../collection.ts";
import { addOptions, ChangeEvent, Options } from "../storage/mod.ts";
import { Document, Filter, WithId } from "../types.ts";
import { Store } from "./store.ts";
export function observe<TSchema extends Document = Document>(
collection: Collection<TSchema>,
filter: Filter<WithId<TSchema>>,
options: Options | undefined,
onChange: (documents: WithId<TSchema>[], changed: WithId<TSchema>[], type: ChangeEvent<TSchema>["type"]) => void
onChange: (documents: WithId<TSchema>[], changed: WithId<TSchema>[], type: ChangeEvent<TSchema>["type"]) => void,
): {
unsubscribe: () => void;
} {
const store = Store.create<TSchema>();
let debounce: NodeJS.Timeout;
let debounce: any;
collection.find(filter, options).then(async (documents) => {
const resolved = await store.resolve(documents);
@@ -51,7 +51,7 @@ export function observe<TSchema extends Document = Document>(
});
}, 0);
}
})
}),
];
return {
@@ -60,13 +60,13 @@ export function observe<TSchema extends Document = Document>(
subscription.unsubscribe();
}
store.destroy();
}
},
};
}
function applyQueryOptions<TSchema extends Document = Document>(
documents: WithId<TSchema>[],
options?: Options
options?: Options,
): WithId<TSchema>[] {
if (options !== undefined) {
return addOptions(new Query({}).find<TSchema>(documents), options).all() as WithId<TSchema>[];

View File

@@ -1,15 +1,13 @@
import { nanoid } from "nanoid";
import { ObserverStorage } from "../Databases/Observer.Storage.js";
import { Storage } from "../Storage/mod.js";
import { Document, Filter, WithId } from "../Types.js";
import { isMatch } from "./IsMatch.js";
import { ObserverStorage } from "../databases/observer/storage.ts";
import { Storage } from "../storage/mod.ts";
import { Document, Filter, WithId } from "../types.ts";
import { isMatch } from "./is-match.ts";
export class Store<TSchema extends Document = Document> {
private constructor(private storage: Storage<TSchema>) {}
static create<TSchema extends Document = Document>() {
return new Store<TSchema>(new ObserverStorage<TSchema>(`observer[${nanoid()}]`));
return new Store<TSchema>(new ObserverStorage<TSchema>(`observer[${crypto.randomUUID()}]`));
}
get destroy() {

View File

@@ -1,14 +1,17 @@
import { RawObject } from "mingo/types";
import { Document } from "../Types.js";
import type { Storage } from "./Storage.js";
import { Document } from "../types.ts";
import type { Storage } from "./storage.ts";
export class DuplicateDocumentError extends Error {
readonly type = "DuplicateDocumentError";
constructor(readonly document: Document, storage: Storage) {
constructor(
readonly document: Document,
storage: Storage,
) {
super(
`Collection Insert Violation: Document '${document.id}' already exists in ${storage.name} collection ${storage.id}`
`Collection Insert Violation: Document '${document.id}' already exists in ${storage.name} collection ${storage.id}`,
);
}
}

5
src/storage/mod.ts Normal file
View File

@@ -0,0 +1,5 @@
export * from "./errors.ts";
export * from "./operators/insert.ts";
export * from "./operators/remove.ts";
export * from "./operators/update.ts";
export * from "./storage.ts";

View File

@@ -1,4 +1,4 @@
import type { Document } from "../../../Types.js";
import type { Document } from "../../types.ts";
export function getInsertManyResult(documents: Document[]): InsertManyResult {
return {
@@ -7,14 +7,14 @@ export function getInsertManyResult(documents: Document[]): InsertManyResult {
insertedIds: documents.reduce<{ [key: number]: string | number }>((map, document, index) => {
map[index] = document.id;
return map;
}, {})
}, {}),
};
}
export function getInsertOneResult(document: Document): InsertOneResult {
return {
acknowledged: true,
insertedId: document.id
insertedId: document.id,
};
}

View File

@@ -0,0 +1,6 @@
export class UpdateResult {
constructor(
readonly matched = 0,
readonly modified = 0,
) {}
}

View File

@@ -1,17 +1,18 @@
import { UpdateOptions } from "mingo/core";
import { Cursor } from "mingo/cursor";
import { nanoid } from "nanoid";
import { UpdateExpression } from "mingo/updater";
import { Subject } from "rxjs";
import { BroadcastChannel, StorageBroadcast } from "../Broadcast.js";
import { Document, Filter, UpdateFilter, WithId } from "../Types.js";
import { InsertManyResult, InsertOneResult } from "./Operators/Insert/mod.js";
import { RemoveResult } from "./Operators/Remove/mod.js";
import { UpdateResult } from "./Operators/Update/mod.js";
import { BroadcastChannel, StorageBroadcast } from "../broadcast.ts";
import { Document, Filter, WithId } from "../types.ts";
import { InsertManyResult, InsertOneResult } from "./operators/insert.ts";
import { RemoveResult } from "./operators/remove.ts";
import { UpdateResult } from "./operators/update.ts";
export abstract class Storage<TSchema extends Document = Document> {
readonly observable = {
change: new Subject<ChangeEvent<TSchema>>(),
flush: new Subject<void>()
flush: new Subject<void>(),
};
status: Status = "loading";
@@ -20,7 +21,7 @@ export abstract class Storage<TSchema extends Document = Document> {
constructor(
readonly name: string,
readonly id = nanoid()
readonly id = crypto.randomUUID(),
) {
this.#channel = new BroadcastChannel(`valkyr:db:${name}`);
this.#channel.onmessage = ({ data }: MessageEvent<StorageBroadcast<TSchema>>) => {
@@ -98,9 +99,21 @@ export abstract class Storage<TSchema extends Document = Document> {
abstract find(filter?: Filter<WithId<TSchema>>, options?: Options): Promise<WithId<TSchema>[]>;
abstract updateOne(filter: Filter<WithId<TSchema>>, operators: UpdateFilter<TSchema>): Promise<UpdateResult>;
abstract updateOne(
filter: Filter<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): Promise<UpdateResult>;
abstract updateMany(filter: Filter<WithId<TSchema>>, operators: UpdateFilter<TSchema>): Promise<UpdateResult>;
abstract updateMany(
filter: Filter<WithId<TSchema>>,
expr: UpdateExpression,
arrayFilters?: Filter<WithId<TSchema>>[],
condition?: Filter<WithId<TSchema>>,
options?: UpdateOptions,
): Promise<UpdateResult>;
abstract replace(filter: Filter<WithId<TSchema>>, document: TSchema): Promise<UpdateResult>;
@@ -129,7 +142,7 @@ export abstract class Storage<TSchema extends Document = Document> {
export function addOptions<TSchema extends Document = Document>(
cursor: Cursor<TSchema>,
options: Options
options: Options,
): Cursor<TSchema> {
if (options.sort) {
cursor.sort(options.sort);

View File

@@ -129,11 +129,12 @@ type Flatten<Type> = Type extends ReadonlyArray<infer Item> ? Item : Type;
type IsAny<Type, ResultIfAny, ResultIfNotAny> = true extends false & Type ? ResultIfAny : ResultIfNotAny;
type FilterOperations<T> = T extends Record<string, any>
? {
[key in keyof T]?: FilterOperators<T[key]>;
}
: FilterOperators<T>;
type FilterOperations<T> =
T extends Record<string, any>
? {
[key in keyof T]?: FilterOperators<T[key]>;
}
: FilterOperators<T>;
type ArrayOperator<Type> = {
$each?: Array<Flatten<Type>>;