import { type Subset } from "../utils/types"; import { type AdvancedCondition } from "../data/advanced-query/query"; import { HashTable, type HashTableItem } from "../data/hash-table/table"; import { Chain, type CommitResult } from "./chain"; import type { DeleteObserver, GenericObserver, InsertObserver, UpdateObserver } from "./observer"; export interface CollectionOptions { primaryKey?: IndexKeys | IndexKeys[]; /** * By default, the data object that is initially passed in is mutated when * changes are committed. Setting `copy` to `true` creates a deep clone of * the data on instantiation so the original array of objects isn't mutated. * * @defaultValue `false` */ copy?: boolean; } export type AssertionFunction = (arg0: Chain) => boolean; export type FindPredicate = (value: DataShape, index: number, array: Y[]) => value is DataShape; export interface Chainable { readonly data: DataResponse; readonly nodes: HashTableItem[]; readonly count: number; readonly exists: boolean; readonly or: any; readonly chain: Chain>; assert: (assertionFnOrDescription: AssertionFunction | string, assertionFn?: AssertionFunction) => Chainable[], Properties>; commit: () => CommitResult; delete: () => Chainable[], Properties>; insert: (value: Data | Data[]) => Chainable[], Properties>; get: (value: unknown) => Chainable, Properties>; find: (value?: unknown) => Chainable[], Properties>; limit: (amount: number) => Chainable[], Properties>; offset: (amount: number) => Chainable[], Properties>; orderBy: (order: Partial>) => Chainable[], Properties>; replace: (value: Data | ((item: Data) => Data)) => Chainable[], Properties>; select: (properties: K[]) => Chainable, K>; set: (value: Partial | ((item: Data) => Partial)) => Chainable[], Properties>; } /** * When thinking of data sources expressed in JSON, you will often have arrays/lists of data objects of a given type: * * ```json * [ * { "name": "Isaac Newton", "born": "1643-01-04T12:00:00.000Z" }, * { "name": "Albert Einstein", "born": "1879-03-14T12:00:00.000Z" } * ] * ``` * * Newton defines an array of `objects` as a `Collection`. * * One might also have an object data structure which contains multiple named collections: * * ```json * { * "scientists": [ * { "name": "Isaac Newton", "born": "1643-01-04T12:00:00.000Z" }, * { "name": "Albert Einstein", "born": "1879-03-14T12:00:00.000Z" } * ], * "universities": [ * { "name": "University of Zurich", "location": "Zurich, Switzerland" } * ] * } * ``` * * Newton defines such a data structure as a `Database` containing multiple `Collections`. * * @template DataShape * The shape of the data structure contained within your collection. Will be statically * inferred from correctly typed data, otherwise you can pass through a type: * * ```ts * interface Scientist { * name: string; * age?: number; * alive: boolean; * gender: "male" | "female"; * } * * const data = await remoteApiCall() as unknown[]; * * // either: * const collection = new Collection(data); * const collection = new Collection(data as unknown as Scientist[]); * ``` * * @template IndexKeys - A union of keys from your `DataShape` that define the primary * key. Default to a union of all keys when no primary key is set. Will be inferred from * the `primaryKey` option set during instantiation. * * ```ts * const collection = new Collection(data, { primaryKey: ["name", "dob"] }); * // IndexKeys will be "name" | "dob" * ``` * * @category Data */ export declare class Collection> { private options; hashTable: HashTable; private primaryKey; private observers; private observer; constructor(data: DataShape[], options?: CollectionOptions); /** * Returns an array of data as it currently exists within your chain. * * @example * For example, referencing `.data` on the root collection will return an array of all data in your collection: * * ```ts * $.data; * * // => [ { "name": "Isaac Newton", "born": "1643-01-04T12:00:00.000Z" }, ... ] * ``` * * When you start chaining operations, `.data` will return an array of data as it currently exists within your chain: * * ```ts * $.find({ name: "Isaac Newton" }).data; * * // => [ { "name": "Isaac Newton", "born": "1643-01-04T12:00:00.000Z" } ] * ``` */ get data(): DataShape[]; get nodes(): HashTableItem[]; private chain; /** * @throws AssertionError */ private $assert; /** * Runs an `assertion` on your chain, and continues the chain execution if the assertion passes and raises an `AssertionError` when it fails. * * @remarks * Takes as input a function whose single argument is the chain instance and which returns a `boolean`: * * @example * ```ts * import { AssertionError } from "newtondb"; * * try { * $.get({ name: "isaac newton" }) * .assert(({ exists }) => exists) * .set({ university: "unknown" }) * .commit(); * } catch (e: unknown) { * if (e instanceof AssertionError) { * // record does not exist * } * } * ``` * * @example * You can optionally pass through a `string` as the first argument and a `function` as the second to describe your assertion: * * ```ts * import { AssertionError } from "newtondb"; * * try { * $.get({ name: "isaac newton" }) * .assert( * "the record the user is attempting to update exists", * ({ exists }) => exists * ) * .set({ university: "unknown" }) * .commit(); * } catch (e: unknown) { * if (e instanceof AssertionError) { * // record does not exist * } * } * ``` */ private assert; private search; private $get; /** * Returns a single record. Most commonly used when querying your collection by a unique identifier. * * @example * For example: * * ```ts * $.get({ code: "isa" }).data; * * // => { "code": "isa", "name": "Isaac Newton", "university": "berlin" } * ``` * * @example * When your collection has been instantiated with a primary key, and your primary key is a single property whose value is a scalar (e.g. a `string` or a `number`), you can call `.get` with that scalar value and Newton will infer the fact that you're querying against your primary key: * * ```ts * $.get("isa").data; * * // => { "code": "isa", "name": "Isaac Newton", "university": "berlin" } * ``` * * @group Query */ get(value?: FindPredicate> | AdvancedCondition | Index | unknown): Chainable, keyof DataShape>; private $find; /** * * Returns multiple records. * * @example * For example: * * ```ts * $.find({ university: "cambridge" }).data; * * // => [ { "code": "alb", "name": "Albert Einstein", "university": "cambridge" } ] * ``` * * Will return an empty array when no results are found. * * @todo find predicate has value unknown * @group Query */ find(value?: FindPredicate> | AdvancedCondition | Index | unknown): Chainable[], keyof DataShape>; private $select; /** * Returns a subset of an object's properties in the resulting data array. * * @example * By default, when a query returns records, the result includes all of those records' attributes. To only return a subset of an object's properties, call `.select` with an array of properties to return: * * ```ts * $.get({ name: "Isaac Newton" }).select(["university"]).data; * * // => { university: "Cambridge" } * ``` * * @example * Given the result of one operation is fed into another, the order of `select` doesn't matter. The above will produce the same output as: * * ```ts * $.select(["university"]).get({ name: "Isaac Newton" }).data; * * // => { university: "Cambridge" } * ``` * * @group Query */ select(properties: K[]): Chainable; private $set; /** * Updates a set of attributes on one or more records. * * @example * * ```ts * // update isaac newton's college to "n/a" and set isAlive to false * $.find({ name: "Isaac Newton" }) * .set({ college: "n/a", isAlive: false }) * .commit(); * ``` * * `set` can also take as input a function whose first argument is the current value of the record, and which must return a subset of the record to update: * * ```ts * // uppercase all universities using .set * $.set(({ university }) => ({ * university: university.toUpperCase(), * })).commit(); * ``` * * @group Mutate */ set(value: Partial | ((item: DataShape) => DataShape | Partial)): Chainable[], keyof DataShape>; private $replace; /** * Replaces an entire document with a new document. * * @example * * ```ts * const newNewton = { * name: "Isaac Newton", * isAlive: false, * diedOn: "1727-03-31T12:00:00.000Z", * }; * * $.get("Isaac Newton").replace(newNewton).commit(); * ``` * * @example * `replace` can also take as input a function whose first argument is the current value of the record, and which must return a complete new record: * * ```ts * // uppercase all universities using .replace * $.replace((record) => ({ * ...record, * university: university.toUpperCase(), * })).commit(); * ``` * * @group Mutate */ replace(value: DataShape | ((item: DataShape) => DataShape)): Chainable[], keyof DataShape>; private $delete; /** * Deletes one or more records from the collection. * * @remarks * `delete()` doesn't take any arguments. Rather, it deletes the records that currently exist within the chain at the time that it's called. For example: * * @example * ```ts * // delete all records from a collection * $.delete().commit(); * * // delete all scientists from cambridge university * $.find({ university: "cambridge" }).delete().commit(); * * // delete a single record * $.get("isaac newton").delete().commit(); * ``` * * @group Mutate */ delete(): Chainable[], keyof DataShape>; private $insert; /** * Inserts one or more records into the database. * * @example * Inserting a single record: * * ```ts * $.insert({ * name: "Nicolaus Copernicus", * born: "1473-02-19T12:00:00.000Z", * }).commit(); * ``` * * @example * You can insert multiple records by passing through an array of objects to insert: * * ```ts * $.insert([ * { name: "Nicolaus Copernicus", born: "1473-02-19T12:00:00.000Z" }, * { name: "Edwin Hubble", born: "1989-11-10T12:00:00.000Z" }, * ]).commit(); * ``` * * @group Mutate */ insert(items: DataShape | DataShape[]): Chainable[], keyof DataShape>; private $limit; /** * You can use `limit` to only return the first `n` amount of records within your chain: * * @example * ```ts * $.find({ university: "cambridge" }).limit(5).data; * ``` * * Will return the first 5 records with `university` set to `"cambridge"`. * * @remarks * You can use `limit` with `offset` to implement an offset based pagination on your data. * * @group Query */ limit(amount: number): Chainable[], keyof DataShape>; private $offset; /** * `offset` will skip the first `n` records from your query. For example, to skip the first 5 records: * * @example * ```ts * $.find({ university: "cambridge" }).offset(5).data; * ``` * * @example * `offset` can be used with `limit` to implement an offset based pagination: * * ```ts * const pageSize = 10; * const currentPage = 3; * * $.find() * .limit(pageSize) * .offset((currentPage - 1) * pageSize).data; * ``` * * @group Query */ offset(amount: number): Chainable[], keyof DataShape>; private $orderBy; /** * `orderBy` can be used to sort records by one or more properties. It takes as input a single object whose properties are a key of your collection's properties, and whose value is either `asc` (for ascending) or `desc` (for descending). * * @example * Using the below dataset: * * ```ts * const students = [ * { name: "roger galilei", university: "mit" }, * { name: "kip tesla", university: "harvard" }, * { name: "rosalind faraday", university: "harvard" }, * { name: "thomas franklin", university: "mit" }, * { name: "albert currie", university: "harvard" }, * ]; * ``` * * To sort by university in descending order and name in ascending order: * * ```ts * $.orderBy({ university: "desc", name: "asc" }).data; * ``` * * This will produce the following: * * ```json * [ * { "name": "roger galilei", "university": "mit" }, * { "name": "thomas franklin", "university": "mit" }, * { "name": "albert currie", "university": "harvard" }, * { "name": "kip tesla", "university": "harvard" }, * { "name": "rosalind faraday", "university": "harvard" } * ] * ``` * @example * Given the order by which you sort is important, `orderBy()` will adhere to the order of the properties in the object passed through. * * For example, in the above example, `{ university: "desc", name: "asc" }` was passed through. `orderBy` would first sort by `university` in `descending` order, and then by `name` in ascending order. * * If you were to instead pass through `{ name: "asc", university: "desc" }`, `orderBy` would first sort by name in `ascending` order and then by `university` in `descending` order. This would produce a different result: * * ```json * [ * { "name": "albert currie", "university": "harvard" }, * { "name": "kip tesla", "university": "harvard" }, * { "name": "roger galilei", "university": "mit" }, * { "name": "rosalind faraday", "university": "harvard" }, * { "name": "thomas franklin", "university": "mit" } * ] * ``` * * @group Query */ orderBy(order: Partial>): Chainable[], keyof DataShape>; /** * Cancels an observer set with the `.observe()` method. Takes as input a numeric ID (which should correspond to the output of the original `.observe` call). * * @throws ObserverError * @group Observer */ unobserve(observerId: number): void; /** * Sets up a callback that are triggered on committed data mutations. * * @remarks * When mutations to the data source are committed, one or more of the following events will be raised: * * - `insert`: raised when a record is inserted into the collection * - `delete`: raised when a record is deleted from the collection * - `updated`: raised when a record is updated * * You can pass callbacks to the `observe` method that will be triggered when these events occur. * * @example * On insert: * * ```ts * const onInsert = $.observe("insert", (record) => { * // * }); * ``` * * @example * On delete: * * ```ts * const onDelete = $.observe("delete", (record) => { * // * }); * ``` * * @example * On update: * * ```ts * const onUpdate = $.observe("updated", (record, historical) => { * // historical.old = item before update * // historical.new = item after update * }); * ``` * * @example * You can also pass through a wildcard observer which will be triggered on every event: * * ```ts * const wildcardObserver = $.observe((event, data) => { * // event: "insert" | "delete" | "updated" * // data: event data * }); * ``` * * Calls to `.observe()` will return an numeric id of the observer. This id should be passed to `unobserve()` to cancel the observer. * * @throws ObserverError * @group Observer */ observe(operation: "insert", callback: InsertObserver): number; observe(operation: "update", callback: UpdateObserver): number; observe(operation: "delete", callback: DeleteObserver): number; observe(callback: GenericObserver): number; private raiseEvents; private $commit; }