@mantlebee/ts-data
    Preparing search index...

    1.1.0 — the in-memory reader

    Version 1.1.0 is additive: nothing changes for a 1.0.0 consumer, every existing export keeps its signature and semantics. It adds the one reader the package ships, over a plain array, so a DataSource with FilterExtension, SortExtension (or SingleSortExtension) and PaginationExtension runs on local data exactly as it runs against a server, without a second "client mode" in the consumer.


    createInMemoryQueryItemsReader(getItems, options?) returns { reader, countReader }: the items reader of a DataSource and the total count reader of a PaginationExtension, both over the given list.

    const { countReader, reader } = createInMemoryQueryItemsReader(() => users);
    const filtering = new FilterExtension<User>();
    const sorting = new SortExtension<User>();
    const paginating = new PaginationExtension<User>(countReader, 20);
    const dataSource = new DataSource<User>(reader, [
    filtering,
    sorting,
    paginating,
    ]);
    await dataSource.read();
    • getItems returns the current list and is called on every read: return another list to swap the data between two reads without rebuilding the data source.
    • Both readers resolve asynchronously, on the next microtask, so isReading of the data source is observable by the UI.
    • The list and its items are never mutated; every result is a new array.

    The same logic, synchronous, for a consumer that already has a payload:

    • queryItems(items, payload, options?) returns the items matching payload.filters, in the order of payload.sorts, reduced to payload.projections and deduplicated when payload.distinct, cut to payload.paging; without paging every matching item, without any property a copy of the list.
    • countItems(items, payload, options?) returns the number of items matching payload.filters, ignoring sorts and paging; with distinct the duplicates (found on the projections when given) are not counted, so the count agrees with the pages.

    Each step of queryItems is a function of its own, in the in-memory subfolder of the feature it belongs to, so a consumer can reuse one step without the others (e.g. filter a list it sorts itself). Every delegate leaves the list and its items untouched and returns a new array, a copy of the list when it has nothing to apply.

    Feature Delegates
    filtering filterItems(items, filters?, options?), itemMatchesFilterExpression(item, expression, options?), itemMatchesFilter(item, filter, options?)
    sorting sortItems(items, sorts?, options?)
    projection projectItems(items, projections?), distinctItems(items)
    pagination pageItems(items, paging?)

    createInMemoryQueryItemsComparer(options?) resolves the comparison options into the InMemoryQueryItemsComparer the delegates use, for a consumer writing its own.

    Every value, of an item and of a filter, goes through toComparableValue before being compared with ===, < and >:

    Value Compared as
    null, undefined null, the single "unset" value
    number, boolean, other primitives as they are
    string lower-cased, unless options.caseSensitive is true
    Date getTime()
    object whose valueOf() is a primitive that primitive, converted with the same rules
    any other object as it is, by reference

    The valueOf rule lets the value objects of the consumer (money, quantities, identifiers) take part in filters and sorts without being a dependency of this package. options.toComparable(value, field) replaces the rule for the fields that need another one; it receives both sides of every comparison and its result is used as it is.

    • Filters: every FilterOperators member. equal, notEqual, in, notIn by === on the comparables; equalInsensitive ignores the case whatever caseSensitive; greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual and range (with exclude on each limit, null limit = open side) never match an unset value; contains / notContains work on strings (substring) and on lists (membership); startsWith / endsWith compare the text of the values. FiltersExpression trees are honoured recursively at any depth; an empty list or group matches every item.
    • Sorts: stable, by priority in the order of payload.sorts; unset values first ascending, last descending.
    • Projections: every item becomes a new object holding the projected fields only (a field the item lacks is not added); without projections the items are returned whole. Applied after the sorts, so a sort on a field that is not projected still orders the result.
    • Distinct: the first occurrence of every item is kept, in its order; two items are duplicates when they hold the same fields with the same values, compared by value (Dates and nested objects by content). Applied after the projections and before the paging, as a SELECT DISTINCT.
    • Paging: 1-based page, itemsPerPage; a page beyond the last one is empty, not an error.

    QueryItemsReader<TItem, TResult = List<TItem>>, the function resolving a QueryItemsPayload into a result, now lives in query-items next to the payload: the query model and its readers no longer depend on data-source. DataSourceReader is the same type under the name of the data source, like DataSourcePayload for QueryItemsPayload; nothing changes for a consumer.

    Everything is re-exported flat from @mantlebee/ts-data.

    Module Exports
    query-items QueryItemsReader
    query-items/in-memory createInMemoryQueryItemsReader, queryItems, countItems, toComparableValue, createInMemoryQueryItemsComparer, InMemoryQueryItemsComparer, InMemoryQueryItemsReaderOptions, InMemoryQueryItemsReader
    query-items/filtering/in-memory filterItems, itemMatchesFilterExpression, itemMatchesFilter
    query-items/sorting/in-memory sortItems
    query-items/projection/in-memory projectItems, distinctItems
    query-items/pagination/in-memory pageItems

    Nothing to change. A consumer that carries its own in-memory resolution of the payload (e.g. the fixtures of a Storybook) can replace it with createInMemoryQueryItemsReader and drop the copy; the readers are plain DataSourceReaders, so they take the place of the HTTP ones without touching the extensions.