tinybase
Version:
A reactive data store and sync engine.
1,420 lines (1,390 loc) • 140 kB
TypeScript
/**
* The queries module of the TinyBase project provides the ability to create and
* track queries of the data in Store objects.
*
* The main entry point to using the queries module is the createQueries
* function, which returns a new Queries object. That object in turn has methods
* that let you create new query definitions, access their results directly, and
* register listeners for when those results change.
* @packageDocumentation
* @module queries
* @since v2.0.0
*/
import type {
GetResultCell,
JoinedCellIdOrId,
} from '../../_internal/queries/with-schemas/index.d.ts';
import type {
CellIdFromSchema,
TableIdFromSchema,
} from '../../_internal/store/with-schemas/index.d.ts';
import type {Id, IdOrNull, Ids} from '../../common/with-schemas/index.d.ts';
import type {GetIdChanges} from '../../store/index.d.ts';
import type {
Cell,
CellOrUndefined,
GetCell,
NoTablesSchema,
OptionalSchemas,
OptionalTablesSchema,
Store,
} from '../../store/with-schemas/index.d.ts';
/**
* The ResultTable type is the data structure representing the results of a
* query.
*
* A ResultTable is typically accessed with the getResultTable method or
* addResultTableListener method. It is similar to the Table type in the store
* module, but without schema-specific typing, and is a regular JavaScript
* object containing individual ResultRow objects, keyed by their Id.
* @example
* ```js
* import type {ResultTable} from 'tinybase';
*
* export const resultTable: ResultTable = {
* fido: {species: 'dog', color: 'brown'},
* felix: {species: 'cat'},
* };
* ```
* @category Result
* @since v2.0.0
*/
export type ResultTable = {[rowId: Id]: ResultRow};
/**
* The ResultRow type is the data structure representing a single row in the
* results of a query.
*
* A ResultRow is typically accessed with the getResultRow method or
* addResultRowListener method. It is similar to the Row type in the store
* module, but without schema-specific typing, and is a regular JavaScript
* object containing individual ResultCell objects, keyed by their Id.
* @example
* ```js
* import type {ResultRow} from 'tinybase';
*
* export const resultRow: ResultRow = {species: 'dog', color: 'brown'};
* ```
* @category Result
* @since v2.0.0
*/
export type ResultRow = {[cellId: Id]: ResultCell};
/**
* The ResultCell type is the data structure representing a single cell in the
* results of a query.
*
* A ResultCell is typically accessed with the getResultCell method or
* addResultCellListener method. It is similar to the Cell type in the store
* module, but without schema-specific typing, and is a JavaScript string,
* number, or boolean.
* @example
* ```js
* import type {ResultCell} from 'tinybase';
*
* export const resultCell: ResultCell = 'dog';
* ```
* @category Result
* @since v2.0.0
*/
export type ResultCell = string | number | boolean;
/**
* The ResultCellOrUndefined type is the data structure representing a single
* cell in the results of a query, or the value `undefined`.
* @category Result
* @since v2.0.0
*/
export type ResultCellOrUndefined = ResultCell | undefined;
/**
* The Aggregate type describes a custom function that takes an array of Cell
* values and returns an aggregate of them.
*
* There are a number of common predefined aggregators, such as for counting,
* summing, and averaging values. This type is instead used for when you wish to
* use a more complex aggregation of your own devising.
* @param cells The array of Cell values to be aggregated.
* @param length The length of the array of Cell values to be aggregated.
* @returns The value of the aggregation.
* @category Aggregators
* @since v2.0.0
*/
export type Aggregate = (cells: ResultCell[], length: number) => ResultCell;
/**
* The AggregateAdd type describes a function that can be used to optimize a
* custom Aggregate by providing a shortcut for when a single value is added to
* the input values.
*
* Some aggregation functions do not need to recalculate the aggregation of the
* whole set when one value changes. For example, when adding a new number to a
* series, the new sum of the series is the new value added to the previous sum.
*
* If it is not possible to shortcut the aggregation based on just one value
* being added, return `undefined` and the aggregation will be completely
* recalculated.
*
* When possible, if you are providing a custom Aggregate, seek an
* implementation of an AggregateAdd function that can reduce the complexity
* cost of growing the input data set.
* @param current The current value of the aggregation.
* @param add The Cell value being added to the aggregation.
* @param length The length of the array of Cell values in the aggregation.
* @returns The new value of the aggregation.
* @category Aggregators
* @since v2.0.0
*/
export type AggregateAdd = (
current: ResultCell,
add: ResultCell,
length: number,
) => ResultCellOrUndefined;
/**
* The AggregateRemove type describes a function that can be used to optimize a
* custom Aggregate by providing a shortcut for when a single value is removed
* from the input values.
*
* Some aggregation functions do not need to recalculate the aggregation of the
* whole set when one value changes. For example, when removing a number from a
* series, the new sum of the series is the new value subtracted from the
* previous sum.
*
* If it is not possible to shortcut the aggregation based on just one value
* being removed, return `undefined` and the aggregation will be completely
* recalculated. One example might be if you were taking the minimum of the
* values, and the previous minimum is being removed. The whole of the rest of
* the list will need to be re-scanned to find a new minimum.
*
* When possible, if you are providing a custom Aggregate, seek an
* implementation of an AggregateRemove function that can reduce the complexity
* cost of shrinking the input data set.
* @param current The current value of the aggregation.
* @param remove The Cell value being removed from the aggregation.
* @param length The length of the array of Cell values in the aggregation.
* @returns The new value of the aggregation.
* @category Aggregators
* @since v2.0.0
*/
export type AggregateRemove = (
current: ResultCell,
remove: ResultCell,
length: number,
) => ResultCellOrUndefined;
/**
* The AggregateReplace type describes a function that can be used to optimize a
* custom Aggregate by providing a shortcut for when a single value in the input
* values is replaced with another.
*
* Some aggregation functions do not need to recalculate the aggregation of the
* whole set when one value changes. For example, when replacing a number in a
* series, the new sum of the series is the previous sum, plus the new value,
* minus the old value.
*
* If it is not possible to shortcut the aggregation based on just one value
* changing, return `undefined` and the aggregation will be completely
* recalculated.
*
* When possible, if you are providing a custom Aggregate, seek an
* implementation of an AggregateReplace function that can reduce the complexity
* cost of changing the input data set in place.
* @param current The current value of the aggregation.
* @param add The Cell value being added to the aggregation.
* @param remove The Cell value being removed from the aggregation.
* @param length The length of the array of Cell values in the aggregation.
* @returns The new value of the aggregation.
* @category Aggregators
* @since v2.0.0
*/
export type AggregateReplace = (
current: ResultCell,
add: ResultCell,
remove: ResultCell,
length: number,
) => ResultCellOrUndefined;
/**
* The QueryCallback type describes a function that takes a query's Id.
*
* A QueryCallback is provided when using the forEachQuery method, so that you
* can do something based on every query in the Queries object. See that method
* for specific examples.
* @param queryId The Id of the query that the callback can operate on.
* @category Callback
* @since v2.0.0
*/
export type QueryCallback = (queryId: Id) => void;
/**
* The ResultTableCallback type describes a function that takes a ResultTable's
* Id and a callback to loop over each ResultRow within it.
*
* A ResultTableCallback is provided when using the forEachResultTable method,
* so that you can do something based on every ResultTable in the Queries
* object. See that method for specific examples.
* @param tableId The Id of the ResultTable that the callback can operate on.
* @param forEachRow A function that will let you iterate over the ResultRow
* objects in this ResultTable.
* @category Callback
* @since v2.0.0
*/
export type ResultTableCallback = (
tableId: Id,
forEachRow: (rowCallback: ResultRowCallback) => void,
) => void;
/**
* The ResultRowCallback type describes a function that takes a ResultRow's Id
* and a callback to loop over each ResultCell within it.
*
* A ResultRowCallback is provided when using the forEachResultRow method, so
* that you can do something based on every ResultRow in a ResultTable. See that
* method for specific examples.
* @param rowId The Id of the ResultRow that the callback can operate on.
* @param forEachCell A function that will let you iterate over the ResultCell
* values in this ResultRow.
* @category Callback
* @since v2.0.0
*/
export type ResultRowCallback = (
rowId: Id,
forEachCell: (cellCallback: ResultCellCallback) => void,
) => void;
/**
* The ResultCellCallback type describes a function that takes a ResultCell's Id
* and its value.
*
* A ResultCellCallback is provided when using the forEachResultCell method, so
* that you can do something based on every ResultCell in a ResultRow. See that
* method for specific examples.
* @param cellId The Id of the ResultCell that the callback can operate on.
* @param cell The value of the ResultCell.
* @category Callback
* @since v2.0.0
*/
export type ResultCellCallback = (cellId: Id, cell: ResultCell) => void;
/**
* The QueryIdsListener type describes a function that is used to listen
* to Query definitions being added or removed.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* (queries: Queries) => void;
* ```
*
* A QueryIdsListener is provided when using the
* addQueryIdsListener method. See that method for specific examples.
*
* When called, a QueryIdsListener is given a reference to the
* Queries object.
* @param queries A reference to the Queries object that changed.
* @category Listener
* @since v2.0.0
*/
export type QueryIdsListener<Schemas extends OptionalSchemas> = (
queries: Queries<Schemas>,
) => void;
/**
* The ResultTableListener type describes a function that is used to listen to
* changes to a query's ResultTable.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* (
* queries: Queries,
* tableId: Id,
* getCellChange: GetResultCellChange,
* ) => void;
* ```
*
* A ResultTableListener is provided when using the addResultTableListener
* method. See that method for specific examples.
*
* When called, a ResultTableListener is given a reference to the Queries
* object, the Id of the ResultTable that changed (which is the same as the
* query Id), and a GetResultCellChange function that can be used to query
* ResultCell values before and after the change.
*
* You can create new query definitions within the body of this listener, though
* obviously be aware of the possible cascading effects of doing so.
* @param queries A reference to the Queries object that changed.
* @param tableId The Id of the ResultTable that changed, which is also the
* query Id.
* @param getCellChange A function that returns information about any
* ResultCell's changes.
* @category Listener
* @since v2.0.0
*/
export type ResultTableListener<Schemas extends OptionalSchemas> = (
queries: Queries<Schemas>,
tableId: Id,
getCellChange: GetResultCellChange,
) => void;
/**
* The ResultTableCellIdsListener type describes a function that is used to
* listen to changes to the Cell Ids that appear anywhere in a query's
* ResultTable.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* (
* queries: Queries,
* tableId: Id,
* getIdChanges: GetIdChanges | undefined,
* ) => void;
* ```
*
* A ResultTableCellIdsListener is provided when using the
* addResultTableCellIdsListener method. See that method for specific examples.
*
* When called, a ResultTableCellIdsListener is given a reference to the Queries
* object, and the Id of the ResultTable whose Cell Ids changed (which is the
* same as the query Id).
*
* You can create new query definitions within the body of this listener, though
* obviously be aware of the possible cascading effects of doing so.
* @param queries A reference to the Queries object that changed.
* @param tableId The Id of the ResultTable that changed, which is also the
* query Id.
* @category Listener
* @since v4.1.0
*/
export type ResultTableCellIdsListener<Schemas extends OptionalSchemas> = (
queries: Queries<Schemas>,
tableId: Id,
getIdChanges: GetIdChanges | undefined,
) => void;
/**
* The ResultRowCountListener type describes a function that is used to listen
* to changes to the number of ResultRow objects in a query's ResultTable.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* (
* queries: Queries,
* tableId: Id,
* count: number,
* ) => void;
* ```
*
* A ResultRowCountListener is provided when using the addResultRowCountListener
* method. See that method for specific examples.
*
* When called, a ResultRowCountListener is given a reference to the Queries
* object, the Id of the ResultTable whose ResultRow Ids changed (which is the
* same as the query Id), and the count of ResultRow objects in the ResultTable.
*
* You can create new query definitions within the body of this listener, though
* obviously be aware of the possible cascading effects of doing so.
* @param queries A reference to the Queries object that changed.
* @param tableId The Id of the ResultTable that changed, which is also the
* query Id.
* @param count The number of ResultRow objects in the ResultTable.
* @category Listener
* @since v4.1.0
*/
export type ResultRowCountListener<Schemas extends OptionalSchemas> = (
queries: Queries<Schemas>,
tableId: Id,
count: number,
) => void;
/**
* The ResultRowIdsListener type describes a function that is used to listen to
* changes to the ResultRow Ids in a query's ResultTable.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* (
* queries: Queries,
* tableId: Id,
* getIdChanges: GetIdChanges | undefined,
* ) => void;
* ```
*
* A ResultRowIdsListener is provided when using the addResultRowIdsListener
* method. See that method for specific examples.
*
* When called, a ResultRowIdsListener is given a reference to the Queries
* object, and the Id of the ResultTable whose ResultRow Ids changed (which is
* the same as the query Id).
*
* You can create new query definitions within the body of this listener, though
* obviously be aware of the possible cascading effects of doing so.
* @param queries A reference to the Queries object that changed.
* @param tableId The Id of the ResultTable that changed, which is also the
* query Id.
* @category Listener
* @since v2.0.0
*/
export type ResultRowIdsListener<Schemas extends OptionalSchemas> = (
queries: Queries<Schemas>,
tableId: Id,
getIdChanges: GetIdChanges | undefined,
) => void;
/**
* The ResultSortedRowIdsListener type describes a function that is used to
* listen to changes to the sorted ResultRow Ids in a query's ResultTable.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* (
* queries: Queries,
* tableId: Id,
* cellId: Id | undefined,
* descending: boolean,
* offset: number,
* limit: number | undefined,
* sortedRowIds: Ids,
* ) => void;
* ```
*
* A ResultSortedRowIdsListener is provided when using the
* addResultSortedRowIdsListener method. See that method for specific examples.
*
* When called, a ResultSortedRowIdsListener is given a reference to the Queries
* object, the Id of the ResultTable whose ResultRow Ids changed (which is the
* same as the query Id), the ResultCell Id being used to sort them, whether
* descending or not, and the offset and limit of the number of Ids returned,
* for pagination purposes. It also receives the sorted array of Ids itself, so
* that you can use them in the listener without the additional cost of an
* explicit call to getResultSortedRowIds.
*
* You can create new query definitions within the body of this listener, though
* obviously be aware of the possible cascading effects of doing so.
* @param queries A reference to the Queries object that changed.
* @param tableId The Id of the ResultTable that changed, which is also the
* query Id.
* @param cellId The Id of the ResultCell whose values were used for the
* sorting.
* @param descending Whether the sorting was in descending order.
* @param offset The number of ResultRow Ids skipped.
* @param limit The maximum number of ResultRow Ids returned.
* @param sortedRowIds The sorted ResultRow Ids themselves.
* @category Listener
* @since v2.0.0
*/
export type ResultSortedRowIdsListener<Schemas extends OptionalSchemas> = (
queries: Queries<Schemas>,
tableId: Id,
cellId: Id | undefined,
descending: boolean,
offset: number,
limit: number | undefined,
sortedRowIds: Ids,
) => void;
/**
* The ResultRowListener type describes a function that is used to listen to
* changes to a ResultRow in a query's ResultTable.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* (
* queries: Queries,
* tableId: Id,
* rowId: Id,
* getCellChange: GetResultCellChange,
* ) => void;
* ```
*
* A ResultRowListener is provided when using the addResultRowListener method.
* See that method for specific examples.
*
* When called, a ResultRowListener is given a reference to the Queries object,
* the Id of the ResultTable that changed (which is the same as the query Id),
* the Id of the ResultRow that changed, and a GetResultCellChange function that
* can be used to query ResultCell values before and after the change.
*
* You can create new query definitions within the body of this listener, though
* obviously be aware of the possible cascading effects of doing so.
* @param queries A reference to the Queries object that changed.
* @param tableId The Id of the ResultTable that changed, which is also the
* query Id.
* @param rowId The Id of the ResultRow that changed.
* @param getCellChange A function that returns information about any
* ResultCell's changes.
* @category Listener
* @since v2.0.0
*/
export type ResultRowListener<Schemas extends OptionalSchemas> = (
queries: Queries<Schemas>,
tableId: Id,
rowId: Id,
getCellChange: GetResultCellChange,
) => void;
/**
* The ResultCellIdsListener type describes a function that is used to listen to
* changes to the ResultCell Ids in a ResultRow in a query's ResultTable.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* (
* queries: Queries,
* tableId: Id,
* rowId: Id,
* getIdChanges: GetIdChanges | undefined,
* ) => void;
* ```
*
* A ResultCellIdsListener is provided when using the addResultCellIdsListener
* method. See that method for specific examples.
*
* When called, a ResultCellIdsListener is given a reference to the Queries
* object, the Id of the ResultTable that changed (which is the same as the
* query Id), and the Id of the ResultRow whose ResultCell Ids changed.
*
* You can create new query definitions within the body of this listener, though
* obviously be aware of the possible cascading effects of doing so.
* @param queries A reference to the Queries object that changed.
* @param tableId The Id of the ResultTable that changed, which is also the
* query Id.
* @param rowId The Id of the ResultRow that changed.
* @category Listener
* @since v2.0.0
*/
export type ResultCellIdsListener<Schemas extends OptionalSchemas> = (
queries: Queries<Schemas>,
tableId: Id,
rowId: Id,
getIdChanges: GetIdChanges | undefined,
) => void;
/**
* The ResultCellListener type describes a function that is used to listen to
* changes to a ResultCell in a query's ResultTable.
*
* This has schema-based typing. The following is a simplified representation:
*
* ```ts override
* (
* queries: Queries,
* tableId: Id,
* rowId: Id,
* cellId: Id,
* newCell: ResultCell,
* oldCell: ResultCell,
* getCellChange: GetResultCellChange,
* ) => void;
* ```
*
* A ResultCellListener is provided when using the addResultCellListener method.
* See that method for specific examples.
*
* When called, a ResultCellListener is given a reference to the Queries object,
* the Id of the ResultTable that changed (which is the same as the query Id),
* the Id of the ResultRow that changed, and the Id of ResultCell that changed.
* It is also given the new value of the ResultCell, the old value of the
* ResultCell, and a GetResultCellChange function that can be used to query
* ResultCell values before and after the change.
*
* You can create new query definitions within the body of this listener, though
* obviously be aware of the possible cascading effects of doing so.
* @param queries A reference to the Queries object that changed.
* @param tableId The Id of the ResultTable that changed, which is also the
* query Id.
* @param rowId The Id of the ResultRow that changed.
* @param cellId The Id of the ResultCell that changed.
* @param newCell The new value of the ResultCell that changed.
* @param oldCell The old value of the ResultCell that changed.
* @param getCellChange A function that returns information about any
* ResultCell's changes.
* @category Listener
* @since v2.0.0
*/
export type ResultCellListener<Schemas extends OptionalSchemas> = (
queries: Queries<Schemas>,
tableId: Id,
rowId: Id,
cellId: Id,
newCell: ResultCell,
oldCell: ResultCell,
getCellChange: GetResultCellChange,
) => void;
/**
* The GetResultCellChange type describes a function that returns information
* about any ResultCell's changes during a transaction.
*
* A GetResultCellChange function is provided to every listener when called due
* the Store changing. The listener can then fetch the previous value of a
* ResultCell before the current transaction, the new value after it, and a
* convenience flag that indicates that the value has changed.
* @param tableId The Id of the ResultTable to inspect.
* @param rowId The Id of the ResultRow to inspect.
* @param cellId The Id of the ResultCell to inspect.
* @returns A ResultCellChange array containing information about the
* ResultCell's changes.
* @category Listener
* @since v2.0.0
*/
export type GetResultCellChange = (
tableId: Id,
rowId: Id,
cellId: Id,
) => ResultCellChange;
/**
* The ResultCellChange type describes a ResultCell's changes during a
* transaction.
*
* This is returned by the GetResultCellChange function that is provided to
* every listener when called. This array contains the previous value of a
* ResultCell before the current transaction, the new value after it, and a
* convenience flag that indicates that the value has changed.
* @category Listener
* @since v2.0.0
*/
export type ResultCellChange = [
changed: boolean,
oldCell: ResultCellOrUndefined,
newCell: ResultCellOrUndefined,
];
/**
* The QueriesListenerStats type describes the number of listeners registered
* with the Queries object, and can be used for debugging purposes.
*
* A QueriesListenerStats object is returned from the getListenerStats method.
* @category Development
* @since v2.0.0
*/
export type QueriesListenerStats = {
/**
* The number of ResultTableListener functions registered with the Queries
* object.
* @category Stat
* @since v2.0.0
*/
table: number;
/**
* The number of ResultTableCellIdsListener functions registered with the
* Queries object, since v3.3.
* @category Stat
* @since v2.0.0
*/
tableCellIds: number;
/**
* The number of ResultRowCountListener functions registered with the Queries
* object, since v4.1.
* @category Stat
* @since v2.0.0
*/
rowCount: number;
/**
* The number of ResultRowIdsListener functions registered with the Queries
* object.
* @category Stat
* @since v2.0.0
*/
rowIds: number;
/**
* The number of SortedRowIdsListener functions registered with the Queries
* object.
* @category Stat
* @since v2.0.0
*/
sortedRowIds: number;
/**
* The number of ResultRowListener functions registered with the Queries
* object.
* @category Stat
* @since v2.0.0
*/
row: number;
/**
* The number of ResultCellIdsListener functions registered with the Queries
* object.
* @category Stat
* @since v2.0.0
*/
cellIds: number;
/**
* The number of ResultCellListener functions registered with the Queries
* object.
* @category Stat
* @since v2.0.0
*/
cell: number;
};
/**
* The GetTableCell type describes a function that takes a Id and returns the
* Cell value for a particular Row, optionally in a joined Table.
*
* A GetTableCell can be provided when setting query definitions, specifically
* in the Select and Where clauses when you want to create or filter on
* calculated values. See those methods for specific examples.
* @category Callback
* @since v2.0.0
*/
export type GetTableCell<
Schema extends OptionalTablesSchema,
RootTableId extends TableIdFromSchema<Schema>,
> = {
/**
* When called with one parameter, this function will return the value of
* the specified Cell from the query's root Table for the Row being selected
* or filtered.
* @param cellId The Id of the Cell to fetch the value for.
* @returns A Cell value or `undefined`.
* @category Callback
* @since v2.0.0
*/
<RootCellId extends CellIdFromSchema<Schema, RootTableId>>(
cellId: RootCellId,
): CellOrUndefined<Schema, RootTableId, RootCellId>;
/**
* When called with two parameters, this function will return the value of
* the specified Cell from a Table that has been joined in the query, for
* the Row being selected or filtered.
* @param joinedTableId The Id of the Table to fetch the value from. If the
* underlying Table was joined 'as' a different Id, that should instead be
* used.
* @param joinedCellId The Id of the Cell to fetch the value for.
* @returns A Cell value or `undefined`.
* @category Callback
* @since v2.0.0
*/
<
JoinedTableId extends TableIdFromSchema<Schema> | Id,
JoinedCellId extends JoinedCellIdOrId<
Schema,
JoinedTableId
> = JoinedCellIdOrId<Schema, JoinedTableId>,
>(
joinedTableId: JoinedTableId,
joinedCellId: JoinedCellId,
):
| (JoinedTableId extends TableIdFromSchema<Schema>
? Cell<Schema, JoinedTableId, JoinedCellId>
: Cell<any, any, any>)
| undefined;
};
/**
* The Select type describes a function that lets you specify a Cell or
* calculated value for including into the query's result.
*
* The Select function is provided to the third `query` parameter of the
* setQueryDefinition method. A query definition must call the Select function
* at least once, otherwise it will be meaningless and return no data.
* @example
* This example shows a query that selects two Cells from the main query Table.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore().setTable('pets', {
* fido: {species: 'dog', color: 'brown', legs: 4},
* felix: {species: 'cat', color: 'black', legs: 4},
* cujo: {species: 'dog', color: 'black', legs: 4},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select}) => {
* select('species');
* select('color');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {species: 'dog', color: 'brown'}}
* // -> {felix: {species: 'cat', color: 'black'}}
* // -> {cujo: {species: 'dog', color: 'black'}}
* ```
* @example
* This example shows a query that selects two Cells, one from a joined Table.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', ownerId: '1'},
* felix: {species: 'cat', ownerId: '2'},
* cujo: {species: 'dog', ownerId: '3'},
* })
* .setTable('owners', {
* '1': {name: 'Alice'},
* '2': {name: 'Bob'},
* '3': {name: 'Carol'},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, join}) => {
* select('species');
* select('owners', 'name');
* // from pets
* join('owners', 'ownerId');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {species: 'dog', name: 'Alice'}}
* // -> {felix: {species: 'cat', name: 'Bob'}}
* // -> {cujo: {species: 'dog', name: 'Carol'}}
* ```
* @example
* This example shows a query that calculates a value from two underlying Cells.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', ownerId: '1'},
* felix: {species: 'cat', ownerId: '2'},
* cujo: {species: 'dog', ownerId: '3'},
* })
* .setTable('owners', {
* '1': {name: 'Alice'},
* '2': {name: 'Bob'},
* '3': {name: 'Carol'},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, join}) => {
* select(
* (getTableCell) =>
* `${getTableCell('species')} for ${getTableCell('owners', 'name')}`,
* ).as('description');
* join('owners', 'ownerId');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {description: 'dog for Alice'}}
* // -> {felix: {description: 'cat for Bob'}}
* // -> {cujo: {description: 'dog for Carol'}}
* ```
* @category Definition
* @since v2.0.0
*/
export type Select<
Schema extends OptionalTablesSchema,
RootTableId extends TableIdFromSchema<Schema>,
> = {
/**
* Calling this function with one Id parameter will indicate that the query
* should select the value of the specified Cell from the query's root Table.
* @param cellId The Id of the Cell to fetch the value for.
* @returns A SelectedAs object so that the selected Cell Id can be optionally
* aliased.
* @category Definition
* @since v2.0.0
*/
<RootCellId extends CellIdFromSchema<Schema, RootTableId>>(
cellId: RootCellId,
): SelectedAs;
/**
* Calling this function with two parameters will indicate that the query
* should select the value of the specified Cell from a Table that has been
* joined in the query.
* @param joinedTableId The Id of the Table to fetch the value from. If the
* underlying Table was joined 'as' a different Id, that should instead be
* used.
* @param joinedCellId The Id of the Cell to fetch the value for.
* @returns A SelectedAs object so that the selected Cell Id can be optionally
* aliased.
* @category Definition
* @since v2.0.0
*/
<JoinedTableId extends TableIdFromSchema<Schema> | Id>(
joinedTableId: JoinedTableId,
joinedCellId: JoinedCellIdOrId<Schema, JoinedTableId>,
): SelectedAs;
/**
* Calling this function with one callback parameter will indicate that the
* query should select a calculated value, based on one or more Cell values in
* the root Table or a joined Table, or on the root Table's Row Id.
* @param getCell A callback that takes a GetTableCell function and the main
* Table's Row Id. These can be used to programmatically create a calculated
* value from multiple Cell values and the Row Id.
* @returns A SelectedAs object so that the selected Cell Id can be optionally
* aliased.
* @category Definition
* @since v2.0.0
*/
(
getCell: (
getTableCell: GetTableCell<Schema, RootTableId>,
rowId: Id,
) => ResultCellOrUndefined,
): SelectedAs;
};
/**
* The SelectedAs type describes an object returned from calling a Select
* function so that the selected Cell Id can be optionally aliased.
*
* If you are using a callback in the Select cause, it is highly recommended to
* use the 'as' function, since otherwise a machine-generated column name will
* be used.
*
* Note that if two Select clauses are both aliased to the same name (or if two
* columns with the same underlying name are selected, both _without_ aliases),
* only the latter of two will be used in the query.
* @example
* This example shows a query that selects two Cells, one from a joined Table.
* Both are aliased with the 'as' function:
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', ownerId: '1'},
* felix: {species: 'cat', ownerId: '2'},
* cujo: {species: 'dog', ownerId: '3'},
* })
* .setTable('owners', {
* '1': {name: 'Alice'},
* '2': {name: 'Bob'},
* '3': {name: 'Carol'},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, join}) => {
* select('species').as('petSpecies');
* select('owners', 'name').as('ownerName');
* // from pets
* join('owners', 'ownerId');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {petSpecies: 'dog', ownerName: 'Alice'}}
* // -> {felix: {petSpecies: 'cat', ownerName: 'Bob'}}
* // -> {cujo: {petSpecies: 'dog', ownerName: 'Carol'}}
* ```
* @category Definition
* @since v2.0.0
*/
export type SelectedAs = {
/**
* A function that lets you specify an alias for the Cell Id.
* @category Definition
* @since v2.0.0
*/
as: (selectedCellId: Id) => void;
};
/**
* The Join type describes a function that lets you specify a Cell or calculated
* value to join the main query Table to other Tables, by their Row Id.
*
* The Join function is provided to the third `query` parameter of the
* setQueryDefinition method.
*
* You can join zero, one, or many Tables. You can join the same underlying
* Table multiple times, but in that case you will need to use the 'as' function
* to distinguish them from each other.
*
* By default, each join is made from the main query Table to the joined table,
* but it is also possible to connect via an intermediate join Table to a more
* distant join Table.
*
* Because a Join clause is used to identify which unique Row Id of the joined
* Table will be joined to each Row of the root Table, queries follow the 'left
* join' semantics you may be familiar with from SQL. This means that an
* unfiltered query will only ever return the same number of Rows as the main
* Table being queried, and indeed the resulting table (assuming it has not been
* aggregated) will even preserve the root Table's original Row Ids.
* @example
* This example shows a query that joins a single Table by using an Id present
* in the main query Table.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', ownerId: '1'},
* felix: {species: 'cat', ownerId: '2'},
* cujo: {species: 'dog', ownerId: '3'},
* })
* .setTable('owners', {
* '1': {name: 'Alice'},
* '2': {name: 'Bob'},
* '3': {name: 'Carol'},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, join}) => {
* select('species');
* select('owners', 'name');
* // from pets
* join('owners', 'ownerId');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {species: 'dog', name: 'Alice'}}
* // -> {felix: {species: 'cat', name: 'Bob'}}
* // -> {cujo: {species: 'dog', name: 'Carol'}}
* ```
* @example
* This example shows a query that joins the same underlying Table twice, and
* aliases them (and the selected Cell Ids). Note the left-join semantics: Felix
* the cat was bought, but the seller was unknown. The record still exists in
* the ResultTable.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', buyerId: '1', sellerId: '2'},
* felix: {species: 'cat', buyerId: '2'},
* cujo: {species: 'dog', buyerId: '3', sellerId: '1'},
* })
* .setTable('humans', {
* '1': {name: 'Alice'},
* '2': {name: 'Bob'},
* '3': {name: 'Carol'},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, join}) => {
* select('buyers', 'name').as('buyer');
* select('sellers', 'name').as('seller');
* // from pets
* join('humans', 'buyerId').as('buyers');
* join('humans', 'sellerId').as('sellers');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {buyer: 'Alice', seller: 'Bob'}}
* // -> {felix: {buyer: 'Bob'}}
* // -> {cujo: {buyer: 'Carol', seller: 'Alice'}}
* ```
* @example
* This example shows a query that calculates the Id of the joined Table based
* from multiple values in the root Table rather than a single Cell.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', color: 'brown'},
* felix: {species: 'cat', color: 'black'},
* cujo: {species: 'dog', color: 'black'},
* })
* .setTable('colorSpecies', {
* 'brown-dog': {price: 6},
* 'black-dog': {price: 5},
* 'brown-cat': {price: 4},
* 'black-cat': {price: 3},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, join}) => {
* select('colorSpecies', 'price');
* // from pets
* join(
* 'colorSpecies',
* (getCell) => `${getCell('color')}-${getCell('species')}`,
* );
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {price: 6}}
* // -> {felix: {price: 3}}
* // -> {cujo: {price: 5}}
* ```
* @example
* This example shows a query that joins two Tables, one through the
* intermediate other.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', ownerId: '1'},
* felix: {species: 'cat', ownerId: '2'},
* cujo: {species: 'dog', ownerId: '3'},
* })
* .setTable('owners', {
* '1': {name: 'Alice', state: 'CA'},
* '2': {name: 'Bob', state: 'CA'},
* '3': {name: 'Carol', state: 'WA'},
* })
* .setTable('states', {
* CA: {name: 'California'},
* WA: {name: 'Washington'},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, join}) => {
* select(
* (getTableCell) =>
* `${getTableCell('species')} in ${getTableCell('states', 'name')}`,
* ).as('description');
* // from pets
* join('owners', 'ownerId');
* join('states', 'owners', 'state');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {description: 'dog in California'}}
* // -> {felix: {description: 'cat in California'}}
* // -> {cujo: {description: 'dog in Washington'}}
* ```
* @category Definition
* @since v2.0.0
*/
export type Join<
Schema extends OptionalTablesSchema,
RootTableId extends TableIdFromSchema<Schema>,
> = {
/**
* Calling this function with two Id parameters will indicate that the join to
* a Row in an adjacent Table is made by finding its Id in a Cell of the
* query's root Table.
* @param joinedTableId The Id of the Table to join to.
* @param on The Id of the Cell in the root Table that contains the joined
* Table's Row Id.
* @returns A JoinedAs object so that the joined Table Id can be optionally
* aliased.
* @category Definition
* @since v2.0.0
*/
(
joinedTableId: TableIdFromSchema<Schema>,
on: CellIdFromSchema<Schema, RootTableId>,
): JoinedAs;
/**
* Calling this function with two parameters (where the second is a function)
* will indicate that the join to a Row in an adjacent Table is made by
* calculating its Id from the Cells and the Row Id of the query's root Table.
* @param joinedTableId The Id of the Table to join to.
* @param on A callback that takes a GetCell function and the root Table's Row
* Id. These can be used to programmatically calculate the joined Table's Row
* Id.
* @returns A JoinedAs object so that the joined Table Id can be optionally
* aliased.
* @category Definition
* @since v2.0.0
*/
(
joinedTableId: TableIdFromSchema<Schema>,
on: (getCell: GetCell<Schema, RootTableId>, rowId: Id) => Id | undefined,
): JoinedAs;
/**
* Calling this function with three Id parameters will indicate that the join
* to a Row in distant Table is made by finding its Id in a Cell of an
* intermediately joined Table.
* @param joinedTableId The Id of the distant Table to join to.
* @param fromIntermediateJoinedTableId The Id of an intermediate Table (which
* should have been in turn joined to the main query table via other Join
* clauses).
* @param on The Id of the Cell in the intermediate Table that contains the
* joined Table's Row Id.
* @returns A JoinedAs object so that the joined Table Id can be optionally
* aliased.
* @category Definition
* @since v2.0.0
*/
<
IntermediateJoinedTableId extends TableIdFromSchema<Schema> | Id =
| TableIdFromSchema<Schema>
| Id,
IntermediateJoinedCellId extends JoinedCellIdOrId<
Schema,
IntermediateJoinedTableId
> = JoinedCellIdOrId<Schema, IntermediateJoinedTableId>,
>(
joinedTableId: TableIdFromSchema<Schema>,
fromIntermediateJoinedTableId: IntermediateJoinedTableId,
on: IntermediateJoinedCellId,
): JoinedAs;
/**
* Calling this function with three parameters (where the third is a function)
* will indicate that the join to a Row in distant Table is made by
* calculating its Id from the Cells and the Row Id of an intermediately
* joined Table.
* @param joinedTableId The Id of the Table to join to.
* @param fromIntermediateJoinedTableId The Id of an intermediate Table (which
* should have been in turn joined to the main query table via other Join
* clauses).
* @param on A callback that takes a GetCell function and the intermediate
* Table's Row Id. These can be used to programmatically calculate the joined
* Table's Row Id.
* @returns A JoinedAs object so that the joined Table Id can be optionally
* aliased.
* @category Definition
* @since v2.0.0
*/
<
IntermediateJoinedTableId extends TableIdFromSchema<Schema> | Id =
| TableIdFromSchema<Schema>
| Id,
>(
joinedTableId: TableIdFromSchema<Schema>,
fromIntermediateJoinedTableId: IntermediateJoinedTableId,
on: (
// prettier-ignore
getIntermediateJoinedCell:
IntermediateJoinedTableId extends TableIdFromSchema<Schema>
? GetCell<Schema, IntermediateJoinedTableId>
: GetCell<NoTablesSchema, Id>,
intermediateJoinedRowId: Id,
) => Id | undefined,
): JoinedAs;
};
/**
* The JoinedAs type describes an object returned from calling a Join function
* so that the joined Table Id can be optionally aliased.
*
* Note that if two Join clauses are both aliased to the same name (or if you
* create two joins to the same underlying Table, both _without_ aliases), only
* the latter of two will be used in the query.
*
* For the purposes of clarity, it's recommended to use an alias that does not
* collide with a real underlying Table (whether included in the query or not).
* @example
* This example shows a query that joins the same underlying Table twice, for
* different purposes. Both joins are aliased with the 'as' function to
* disambiguate them. Note that the selected Cells are also aliased.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', buyerId: '1', sellerId: '2'},
* felix: {species: 'cat', buyerId: '2'},
* cujo: {species: 'dog', buyerId: '3', sellerId: '1'},
* })
* .setTable('humans', {
* '1': {name: 'Alice'},
* '2': {name: 'Bob'},
* '3': {name: 'Carol'},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, join}) => {
* select('buyers', 'name').as('buyer');
* select('sellers', 'name').as('seller');
* // from pets
* join('humans', 'buyerId').as('buyers');
* join('humans', 'sellerId').as('sellers');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {buyer: 'Alice', seller: 'Bob'}}
* // -> {felix: {buyer: 'Bob'}}
* // -> {cujo: {buyer: 'Carol', seller: 'Alice'}}
* ```
* @category Definition
* @since v2.0.0
*/
export type JoinedAs = {
/**
* A function that lets you specify an alias for the joined Table Id.
* @category Definition
* @since v2.0.0
*/
as: (joinedTableId: Id) => void;
};
/**
* The Where type describes a function that lets you specify conditions to
* filter results, based on the underlying Cells of the root or joined Tables.
*
* The Where function is provided to the third `query` parameter of the
* setQueryDefinition method.
*
* If you do not specify a Where clause, you should expect every non-empty Row
* of the root Table to appear in the query's results.
*
* A Where condition has to be true for a Row to be included in the results.
* Each Where class is additive, as though combined with a logical 'and'. If you
* wish to create an 'or' expression, use the single parameter version of the
* type that allows arbitrary programmatic conditions.
*
* The Where keyword differs from the Having keyword in that the former
* describes conditions that should be met by underlying Cell values (whether
* selected or not), and the latter describes conditions based on calculated and
* aggregated values - after Group clauses have been applied.
* @example
* This example shows a query that filters the results from a single Table by
* comparing an underlying Cell from it with a value.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore().setTable('pets', {
* fido: {species: 'dog'},
* felix: {species: 'cat'},
* cujo: {species: 'dog'},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, where}) => {
* select('species');
* where('species', 'dog');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {species: 'dog'}}
* // -> {cujo: {species: 'dog'}}
* ```
* @example
* This example shows a query that filters the results of a query by comparing
* an underlying Cell from a joined Table with a value. Note that the joined
* table has also been aliased, and so its alias is used in the Where clause.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', ownerId: '1'},
* felix: {species: 'cat', ownerId: '2'},
* cujo: {species: 'dog', ownerId: '3'},
* })
* .setTable('owners', {
* '1': {name: 'Alice', state: 'CA'},
* '2': {name: 'Bob', state: 'CA'},
* '3': {name: 'Carol', state: 'WA'},
* });
*
* const queries = createQueries(store);
* queries.setQueryDefinition('query', 'pets', ({select, join, where}) => {
* select('species');
* // from pets
* join('owners', 'ownerId').as('petOwners');
* where('petOwners', 'state', 'CA');
* });
*
* queries.forEachResultRow('query', (rowId) => {
* console.log({[rowId]: queries.getResultRow('query', rowId)});
* });
* // -> {fido: {species: 'dog'}}
* // -> {felix: {species: 'cat'}}
* ```
* @example
* This example shows a query that filters the results of a query with a
* condition that is calculated from underlying Cell values from the main and
* joined Table. Note that the joined table has also been aliased, and so its
* alias is used in the Where clause.
*
* ```js
* import {createQueries, createStore} from 'tinybase';
*
* const store = createStore()
* .setTable('pets', {
* fido: {species: 'dog', ownerId: '1'},
* felix: {species: 'cat', ownerId: '2'},
* cujo: {species: 'dog', ownerId: '3'},
* })
* .setTable('owners', {
* '1': {name: 'Alice', state: '