UNPKG

tinybase

Version:

A reactive data store and sync engine.

1,420 lines (1,390 loc) 140 kB
/** * 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: '