feat: release 3.0.0

This commit is contained in:
2026-01-05 23:19:42 +01:00
parent ed15a0eb27
commit 4aaf12948c
25 changed files with 1799 additions and 1812 deletions

View File

@@ -4,10 +4,10 @@ import type { Modifier } from "mingo/updater";
import type { ZodObject, ZodRawShape } from "zod";
import z from "zod";
import type { IndexSpec } from "./index/manager.ts";
import { observe, observeOne } from "./observe/mod.ts";
import type { Index } from "./registrars.ts";
import type { ChangeEvent, QueryOptions, Storage, UpdateResult } from "./storage.ts";
import type { AnyDocument } from "./types.ts";
import type { AnyDocument, QueryCriteria } from "./types.ts";
/*
|--------------------------------------------------------------------------------
@@ -44,8 +44,8 @@ export class Collection<
get primaryKey(): string {
for (const index of this.options.indexes ?? []) {
if (index[1]?.primary === true) {
return index[0] as string;
if (index.kind === "primary") {
return index.field;
}
}
throw new Error(`Collection '${this.name}' is missing required primary key assignment.`);
@@ -57,7 +57,7 @@ export class Collection<
|--------------------------------------------------------------------------------
*/
getPrimaryKeyValue(document: AnyDocument): string | number {
getPrimaryKeyValue(document: AnyDocument): string {
const id = document[this.#pkey];
if (id === undefined || typeof id !== "string") {
throw new Error(
@@ -74,7 +74,9 @@ export class Collection<
*/
async insert(documents: TSchema[]): Promise<void> {
return this.storage.resolve().then((storage) => storage.insert(documents));
return this.storage
.resolve()
.then((storage) => storage.insert(documents.map((document) => this.#schema.parse(document))));
}
async update(
@@ -122,7 +124,7 @@ export class Collection<
* Performs a mingo filter search over the collection data and returns
* a single document if one was found matching the filter and options.
*/
async findOne(condition: Criteria<TSchema> = {}, options: QueryOptions = {}): Promise<TSchema | undefined> {
async findOne(condition: QueryCriteria<TSchema> = {}, options: QueryOptions = {}): Promise<TSchema | undefined> {
return this.findMany(condition, { ...options, limit: 1 }).then(([document]) => document);
}
@@ -130,7 +132,7 @@ export class Collection<
* Performs a mingo filter search over the collection data and returns any
* documents matching the provided filter and options.
*/
async findMany(condition: Criteria<TSchema> = {}, options?: QueryOptions): Promise<TSchema[]> {
async findMany(condition: QueryCriteria<TSchema> = {}, options?: QueryOptions): Promise<TSchema[]> {
return this.storage
.resolve()
.then((storage) =>
@@ -144,17 +146,17 @@ export class Collection<
* Performs a mingo filter search over the collection data and returns
* the count of all documents found matching the filter and options.
*/
async count(condition?: Criteria<TSchema>): Promise<number> {
return this.storage.resolve().then((storage) => storage.count({ collection: this.options.name, condition }));
async count(condition: Criteria<TSchema> = {}): Promise<number> {
return this.storage.resolve().then((storage) => storage.count(condition));
}
/**
* Removes all documents from the storage instance.
*/
flush(): void {
this.storage.resolve().then((storage) => {
async flush(): Promise<void> {
await this.storage.resolve().then(async (storage) => {
await storage.flush();
storage.broadcast("flush");
storage.flush();
});
}
@@ -216,5 +218,5 @@ type CollectionOptions<TName extends string, TStorage extends Storage, TSchema e
/**
* List of custom indexes for the collection.
*/
indexes: Index[];
indexes: IndexSpec<TSchema>[];
};

View File

@@ -1,8 +1,9 @@
import { type IDBPDatabase, openDB } from "idb";
import { Collection } from "../../collection.ts";
import type { IndexSpec } from "../../index/manager.ts";
import type { DBLogger } from "../../logger.ts";
import type { Index, Registrars } from "../../registrars.ts";
import type { Registrars } from "../../registrars.ts";
import { IndexedDBStorage } from "./storage.ts";
export class IndexedDB<TOptions extends IndexedDBOptions> {
@@ -12,10 +13,22 @@ export class IndexedDB<TOptions extends IndexedDBOptions> {
constructor(readonly options: TOptions) {
this.#db = openDB(options.name, options.version ?? 1, {
upgrade: (db: IDBPDatabase) => {
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, indexes } of options.registrars) {
const store = db.createObjectStore(name, {
keyPath: indexes.find((index: IndexSpec<any>) => index.kind === "primary")?.field,
});
for (const { field, kind } of indexes) {
switch (kind) {
case "primary":
case "unique": {
store.createIndex(field, field, { unique: true });
break;
}
case "shared": {
store.createIndex(field, field);
break;
}
}
}
}
},
@@ -48,7 +61,7 @@ export class IndexedDB<TOptions extends IndexedDBOptions> {
name: TName;
storage: IndexedDBStorage;
schema: TSchema;
indexes: Index[];
indexes: IndexSpec<any>[];
}> {
const collection = this.#collections.get(name);
if (collection === undefined) {
@@ -73,8 +86,8 @@ export class IndexedDB<TOptions extends IndexedDBOptions> {
}
}
close() {
this.#db.then((db) => db.close());
async close() {
await this.#db.then((db) => db.close());
}
}

View File

@@ -3,8 +3,8 @@ import { Query, update } from "mingo";
import type { Criteria } from "mingo/types";
import type { Modifier } from "mingo/updater";
import type { IndexSpec } from "../../index/manager.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";
@@ -21,7 +21,7 @@ export class IndexedDBStorage<TSchema extends AnyDocument = AnyDocument> extends
constructor(
name: string,
indexes: Index[],
indexes: IndexSpec<TSchema>[],
promise: Promise<IDBPDatabase>,
readonly log: DBLogger = function log() {},
) {
@@ -29,21 +29,6 @@ export class IndexedDBStorage<TSchema extends AnyDocument = AnyDocument> extends
this.#promise = promise;
}
async resolve() {
if (this.#db === undefined) {
this.#db = await this.#promise;
}
return this;
}
async has(id: string): Promise<boolean> {
const document = await this.db.getFromIndex(this.name, "id", id);
if (document !== undefined) {
return true;
}
return false;
}
get db() {
if (this.#db === undefined) {
throw new Error("Database not initialized");
@@ -51,6 +36,13 @@ export class IndexedDBStorage<TSchema extends AnyDocument = AnyDocument> extends
return this.#db;
}
async resolve() {
if (this.#db === undefined) {
this.#db = await this.#promise;
}
return this;
}
/*
|--------------------------------------------------------------------------------
| Insert

View File

@@ -1,5 +1,6 @@
import { Collection } from "../../collection.ts";
import type { Index, Registrars } from "../../registrars.ts";
import type { IndexSpec } from "../../index/manager.ts";
import type { Registrars } from "../../registrars.ts";
import { MemoryStorage } from "./storage.ts";
export class MemoryDatabase<TOptions extends MemoryDatabaseOptions> {
@@ -42,7 +43,7 @@ export class MemoryDatabase<TOptions extends MemoryDatabaseOptions> {
name: TName;
storage: MemoryStorage;
schema: TSchema;
indexes: Index[];
indexes: IndexSpec<any>[];
}> {
const collection = this.#collections.get(name);
if (collection === undefined) {

View File

@@ -5,18 +5,18 @@ import type { Modifier } from "mingo/updater";
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";
import type { AnyDocument, StringKeyOf } from "../../types.ts";
export class MemoryStorage<TSchema extends AnyDocument = AnyDocument> extends Storage<TSchema> {
readonly index: IndexManager<TSchema>;
constructor(name: string, indexes: IndexSpec[]) {
constructor(name: string, indexes: IndexSpec<TSchema>[]) {
super(name, indexes);
this.index = new IndexManager(indexes);
}
get documents() {
return this.index.primary.tree;
return this.index.primary.documents;
}
async resolve() {
@@ -30,14 +30,14 @@ export class MemoryStorage<TSchema extends AnyDocument = AnyDocument> extends St
this.broadcast("insert", documents);
}
async getByIndex(index: string, value: string): Promise<TSchema[]> {
return this.index.get(index)?.get(value) ?? [];
async getByIndex(field: StringKeyOf<TSchema>, value: string): Promise<TSchema[]> {
return this.index.getByIndex(field, value);
}
async find(condition: Criteria<TSchema> = {}, options?: QueryOptions): Promise<TSchema[]> {
let cursor = new Query(condition).find<TSchema>(this.documents);
const cursor = new Query(condition).find<TSchema>(this.documents);
if (options !== undefined) {
cursor = addOptions(cursor, options);
return addOptions(cursor, options).all();
}
return cursor.all();
}
@@ -58,7 +58,7 @@ export class MemoryStorage<TSchema extends AnyDocument = AnyDocument> extends St
if (modified.length > 0) {
modifiedCount += 1;
documents.push(document);
this.documents.add(document);
this.index.update(document);
}
}
@@ -72,7 +72,7 @@ export class MemoryStorage<TSchema extends AnyDocument = AnyDocument> extends St
async remove(condition: Criteria<TSchema>): Promise<number> {
const documents = await this.find(condition);
for (const document of documents) {
this.documents.delete(document);
this.index.remove(document);
}
this.broadcast("remove", documents);
return documents.length;
@@ -83,6 +83,6 @@ export class MemoryStorage<TSchema extends AnyDocument = AnyDocument> extends St
}
async flush(): Promise<void> {
this.documents.clear();
this.index.flush();
}
}

View File

@@ -1,29 +0,0 @@
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);
});
});

View File

@@ -1,17 +1,19 @@
import type { Criteria } from "mingo/types";
import type { AnyDocument } from "../types.ts";
import { PrimaryIndex } from "./primary.ts";
import type { AnyDocument, StringKeyOf } from "../types.ts";
import { PrimaryIndex, type PrimaryKey } from "./primary.ts";
import { SharedIndex } from "./shared.ts";
import { UniqueIndex } from "./unique.ts";
const EMPTY_SET: ReadonlySet<PrimaryKey> = Object.freeze(new Set<PrimaryKey>());
export class IndexManager<TSchema extends AnyDocument> {
readonly primary: PrimaryIndex<TSchema>;
readonly unique = new Map<keyof TSchema, UniqueIndex>();
readonly shared = new Map<keyof TSchema, SharedIndex>();
readonly unique = new Map<StringKeyOf<TSchema>, UniqueIndex>();
readonly shared = new Map<StringKeyOf<TSchema>, SharedIndex>();
constructor(specs: IndexSpec[]) {
constructor(readonly specs: IndexSpec<TSchema>[]) {
const primary = specs.find((spec) => spec.kind === "primary");
if (primary === undefined) {
throw new Error("Primary index is required");
@@ -31,43 +33,165 @@ export class IndexManager<TSchema extends AnyDocument> {
}
}
/**
* Atomic insert of the document into the index pools. If any part
* of the operation fails all changes are rolled back to their original
* states.
*
* @param document - Document to insert.
*/
insert(document: TSchema) {
const pk = document[this.primary.key];
for (const [field, index] of this.unique) {
index.insert(document[field], pk);
const insertedUniques: [StringKeyOf<TSchema>, any][] = [];
const insertedShared: [StringKeyOf<TSchema>, any][] = [];
try {
for (const [field, index] of this.unique) {
index.insert(document[field], pk);
insertedUniques.push([field, document[field]]);
}
for (const [field, index] of this.shared) {
index.insert(document[field], pk);
insertedShared.push([field, document[field]]);
}
this.primary.insert(pk, document);
} catch (err) {
for (const [field, value] of insertedUniques) {
this.unique.get(field)?.delete(value);
}
for (const [field, value] of insertedShared) {
this.shared.get(field)?.delete(value, pk);
}
throw err;
}
for (const [field, index] of this.shared) {
index.insert(document[field], pk);
}
this.primary.insert(pk, document);
}
getByCondition(condition: Criteria<TSchema>): TSchema[] | undefined {
// const pks = new Set<any>();
// for (const key in condition) {
// if (this.indexes.includes(key)) {
// if (key === this.primaryKey) {
// pks.add(condition[key]);
// } else {
// const
// }
// }
// }
return [];
getByCondition(condition: Criteria<TSchema>): TSchema[] {
const indexedKeys = Array.from(
new Set([this.primary.key as StringKeyOf<TSchema>, ...this.unique.keys(), ...this.shared.keys()]),
);
const candidatePKs: PrimaryKey[] = [];
// ### Primary Keys
// Collect primary keys for indexed equality conditions
const pkSets: ReadonlySet<PrimaryKey>[] = [];
for (const key of indexedKeys) {
const value = (condition as any)[key];
if (value !== undefined) {
// Use index if available
const pks = this.getPrimaryKeysByIndex(key, value);
pkSets.push(pks);
}
}
// ### Intersect
// Intersect all sets to find candidates
if (pkSets.length > 0) {
const sortedSets = pkSets.sort((a, b) => a.size - b.size);
const intersection = new Set(sortedSets[0]);
for (let i = 1; i < sortedSets.length; i++) {
for (const pk of intersection) {
if (!sortedSets[i].has(pk)) {
intersection.delete(pk);
}
}
}
candidatePKs.push(...intersection);
} else {
candidatePKs.push(...this.primary.keys()); // no indexed fields → scan all primary keys
}
// ### Filter
// Filter candidates by remaining condition
const results: TSchema[] = [];
for (const pk of candidatePKs) {
const doc = this.primary.get(pk);
if (doc === undefined) {
continue;
}
let match = true;
for (const [field, expected] of Object.entries(condition)) {
if ((doc as any)[field] !== expected) {
match = false;
break;
}
}
if (match) {
results.push(doc);
}
}
return results;
}
/**
* Get all primary keys found for given field => value pair.
*
* @param field - Field to lookup.
* @param value - Value to lookup.
*/
getPrimaryKeysByIndex(field: StringKeyOf<TSchema>, value: any): ReadonlySet<PrimaryKey> {
if (field === this.primary.key) {
if (this.primary.has(value)) {
return new Set([value]);
}
return EMPTY_SET;
}
if (this.unique.has(field)) {
const pk = this.unique.get(field)?.lookup(value);
if (pk === undefined) {
return EMPTY_SET;
}
return new Set([pk]);
}
return this.shared.get(field)?.lookup(value) ?? EMPTY_SET;
}
/**
* Get document by primary key.
*
* @param pk - Primary key to fetch document for.
*/
getByPrimary(pk: string): TSchema | undefined {
return this.primary.get(pk);
}
getByUnique(field: keyof TSchema, value: any): TSchema | undefined {
/**
* Get a document found for given field => value pair.
*
* @param field - Field to lookup.
* @param value - Value to lookup.
*/
getByUnique(field: StringKeyOf<TSchema>, value: any): TSchema | undefined {
const pk = this.unique.get(field)?.lookup(value);
if (pk !== undefined) {
return this.primary.get(pk);
}
}
getByIndex(field: keyof TSchema, value: any): TSchema[] {
/**
* Get all documents found for given field => value pair.
*
* @note This method may clean up stale index entries during reads.
*
* @param field - Field to lookup.
* @param value - Value to lookup.
*/
getByIndex(field: StringKeyOf<TSchema>, value: any): TSchema[] {
if (field === this.primary.key) {
const document = this.getByPrimary(value);
if (document === undefined) {
return [];
}
return [document];
}
if (this.unique.has(field)) {
const document = this.getByUnique(field, value);
if (document === undefined) {
@@ -94,23 +218,94 @@ export class IndexManager<TSchema extends AnyDocument> {
return documents;
}
remove(pk: string) {
const document = this.primary.get(pk);
if (document === undefined) {
/**
* Update indexes for given document.
*
* @note If the document does not exist it will be inserted.
*
* @param document - Document to update against current index.
*/
update(document: TSchema) {
const pk = document[this.primary.key];
const current = this.primary.get(pk);
if (current === undefined) {
return this.insert(document);
}
const revertedUniques: [StringKeyOf<TSchema>, any][] = [];
const revertedShared: [StringKeyOf<TSchema>, any][] = [];
try {
for (const [field, index] of this.unique) {
if (current[field] !== document[field]) {
index.delete(current[field]);
index.insert(document[field], pk);
revertedUniques.push([field, current[field]]);
}
}
for (const [field, index] of this.shared) {
if (current[field] !== document[field]) {
index.delete(current[field], pk);
index.insert(document[field], pk);
revertedShared.push([field, current[field]]);
}
}
this.primary.replace(pk, document);
} catch (err) {
for (const [field, value] of revertedUniques) {
this.unique.get(field)?.insert(value, pk);
this.unique.get(field)?.delete(document[field]);
}
for (const [field, value] of revertedShared) {
this.shared.get(field)?.insert(value, pk);
this.shared.get(field)?.delete(document[field], pk);
}
throw err;
}
}
/**
* Remove all indexes related to given document.
*
* @param document - Document to remove.
*/
remove(document: TSchema) {
const pk = document[this.primary.key];
const current = this.primary.get(pk);
if (current === undefined) {
return;
}
for (const [field, index] of this.unique) {
index.delete(document[field]);
index.delete(current[field]);
}
for (const [field, index] of this.shared) {
index.delete(document[field], pk);
index.delete(current[field], pk);
}
this.primary.delete(pk);
}
flush() {
this.primary.flush();
this.unique.clear();
this.shared.clear();
for (const spec of this.specs) {
switch (spec.kind) {
case "unique": {
this.unique.set(spec.field, new UniqueIndex());
break;
}
case "shared": {
this.shared.set(spec.field, new SharedIndex());
break;
}
}
}
}
}
export type IndexSpec = {
field: string;
export type IndexSpec<TSchema extends AnyDocument> = {
field: StringKeyOf<TSchema>;
kind: IndexKind;
};

View File

@@ -7,6 +7,18 @@ export class PrimaryIndex<TSchema extends AnyDocument> {
constructor(readonly key: string) {}
get documents() {
return Array.from(this.#index.values());
}
keys() {
return Array.from(this.#index.keys());
}
has(pk: PrimaryKey): boolean {
return this.#index.has(pk);
}
insert(pk: PrimaryKey, document: TSchema) {
if (this.#index.has(pk)) {
throw new Error(`Duplicate primary key: ${pk}`);
@@ -18,7 +30,15 @@ export class PrimaryIndex<TSchema extends AnyDocument> {
return this.#index.get(pk);
}
replace(pk: PrimaryKey, document: TSchema) {
this.#index.set(pk, document);
}
delete(pk: PrimaryKey) {
this.#index.delete(pk);
}
flush() {
this.#index.clear();
}
}

View File

@@ -1,5 +1,7 @@
import type { PrimaryKey } from "./primary.ts";
const EMPTY_SET: ReadonlySet<PrimaryKey> = Object.freeze(new Set<PrimaryKey>());
export class SharedIndex {
readonly #index = new Map<string, Set<PrimaryKey>>();
@@ -23,8 +25,8 @@ export class SharedIndex {
*
* @param value - Value to lookup a primary key for.
*/
lookup(value: any): Set<PrimaryKey> {
return this.#index.get(value) ?? new Set();
lookup(value: any): ReadonlySet<PrimaryKey> {
return this.#index.get(value) ?? EMPTY_SET;
}
/**

View File

@@ -13,7 +13,7 @@ export function observe<TCollection extends Collection>(
options: QueryOptions | undefined,
onChange: (documents: AnyDocument[], changed: AnyDocument[], type: ChangeEvent["type"]) => void,
): Subscription {
const documents = new Map<string | number, AnyDocument>();
const cache = new Map<string, AnyDocument>();
let debounce: any;
@@ -21,6 +21,9 @@ export function observe<TCollection extends Collection>(
// Find the initial documents and send them to the change listener.
collection.findMany(condition, options).then(async (documents) => {
for (const document of documents) {
cache.set(collection.getPrimaryKeyValue(document), document);
}
onChange(documents, documents, "insert");
});
@@ -37,7 +40,7 @@ export function observe<TCollection extends Collection>(
case "insert": {
for (const document of data) {
if (isMatch(document, condition)) {
documents.set(collection.getPrimaryKeyValue(document), document);
cache.set(collection.getPrimaryKeyValue(document), document);
changed.push(document);
}
}
@@ -46,15 +49,15 @@ export function observe<TCollection extends Collection>(
case "update": {
for (const document of data) {
const id = collection.getPrimaryKeyValue(document);
if (documents.has(id)) {
if (cache.has(id)) {
if (isMatch(document, condition)) {
documents.set(id, document);
cache.set(id, document);
} else {
documents.delete(id);
cache.delete(id);
}
changed.push(document);
} else if (isMatch(document, condition)) {
documents.set(id, document);
cache.set(id, document);
changed.push(document);
}
}
@@ -63,7 +66,7 @@ export function observe<TCollection extends Collection>(
case "remove": {
for (const document of data) {
if (isMatch(document, condition)) {
documents.delete(collection.getPrimaryKeyValue(document));
cache.delete(collection.getPrimaryKeyValue(document));
changed.push(document);
}
}
@@ -73,7 +76,7 @@ export function observe<TCollection extends Collection>(
if (changed.length > 0) {
clearTimeout(debounce);
debounce = setTimeout(() => {
onChange(applyQueryOptions(Array.from(documents.values()), options), changed, type);
onChange(applyQueryOptions(Array.from(cache.values()), options), changed, type);
}, 0);
}
}),

View File

@@ -1,6 +1,9 @@
import type { AnyDocument } from "@valkyr/db";
import type { ZodRawShape } from "zod";
export type Registrars<TSchema extends ZodRawShape = ZodRawShape> = {
import type { IndexSpec } from "./index/manager.ts";
export type Registrars<TSchema extends AnyDocument = ZodRawShape> = {
/**
* Name of the collection.
*/
@@ -14,5 +17,5 @@ export type Registrars<TSchema extends ZodRawShape = ZodRawShape> = {
/**
* List of custom indexes for the collection.
*/
indexes: IndexSpec[];
indexes: IndexSpec<TSchema>[];
};

View File

@@ -4,7 +4,7 @@ import type { Criteria } from "mingo/types";
import type { Modifier } from "mingo/updater";
import { BroadcastChannel, type StorageBroadcast } from "./broadcast.ts";
import type { Index } from "./registrars.ts";
import type { IndexSpec } from "./index/manager.ts";
import type { AnyDocument } from "./types.ts";
type StorageEvent = "change" | "flush";
@@ -25,10 +25,10 @@ export abstract class Storage<TSchema extends AnyDocument = AnyDocument> {
/**
* List of indexes to optimize storage lookups.
*/
readonly indexes: Index[],
readonly indexes: IndexSpec<TSchema>[],
) {
if (primaryIndexCount(indexes) !== 1) {
throw new Error("Storage is missing or has more than 1 defined primaryIndex");
throw new Error("missing required primary key assignment");
}
this.#channel = new BroadcastChannel(`@valkyr/db:${name}`);
this.#channel.onmessage = ({ data }: MessageEvent<StorageBroadcast>) => {
@@ -174,7 +174,7 @@ export function addOptions<TSchema extends AnyDocument = AnyDocument>(
cursor: Cursor<TSchema>,
options: QueryOptions,
): Cursor<TSchema> {
if (options.sort) {
if (options.sort !== undefined) {
cursor.sort(options.sort);
}
if (options.skip !== undefined) {
@@ -186,10 +186,10 @@ export function addOptions<TSchema extends AnyDocument = AnyDocument>(
return cursor;
}
function primaryIndexCount(indexes: Index[]): number {
function primaryIndexCount<TSchema extends AnyDocument = AnyDocument>(indexes: IndexSpec<TSchema>[]): number {
let count = 0;
for (const [, options] of indexes) {
if (options?.primary === true) {
for (const { kind } of indexes) {
if (kind === "primary") {
count += 1;
}
}

View File

@@ -1,3 +1,5 @@
import type { Criteria } from "mingo/types";
/**
* Represents an unknown document with global support.
*/
@@ -5,7 +7,24 @@ export type AnyDocument = {
[key: string]: any;
};
export type StringKeyOf<T> = Extract<keyof T, string>;
/**
* Simplifies a complex type.
*/
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
/**
* Extended Criteria type that includes MongoDB logical and comparison operators
*/
export type QueryCriteria<T> = Criteria<T> & {
$and?: QueryCriteria<T>[];
$or?: QueryCriteria<T>[];
$nor?: QueryCriteria<T>[];
$not?: QueryCriteria<T>;
$exists?: boolean;
$type?: string | number;
[key: string]: any;
};