DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.6.1
5.33.3 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
  • API Docs
    • Observables
      • can-bind
      • can-compute
      • can-debug
      • can-deep-observable
      • can-define
      • can-define/list/list
      • can-define/map/map
      • can-define-backup
      • can-define-stream
      • can-define-stream-kefir
      • can-event-queue
      • can-kefir
      • can-list
      • can-map
      • can-map-compat
      • can-map-define
      • can-observable-array
      • can-observable-object
      • can-observation
      • can-observation-recorder
      • can-observe
      • can-simple-map
      • can-simple-observable
      • can-stream
      • can-stream-kefir
      • can-value
    • Views
      • can-attribute-observable
      • can-component
      • can-observable-bindings
      • can-stache
      • can-stache-bindings
      • can-stache-converters
      • can-stache-element
      • can-stache-route-helpers
      • can-view-autorender
      • can-view-callbacks
      • can-view-import
      • can-view-live
      • can-view-model
      • can-view-parser
      • can-view-scope
      • can-view-target
      • steal-stache
    • Data Modeling
      • can-connect
      • can-connect-ndjson
      • can-connect-tag
      • can-define-realtime-rest-model
      • can-define-rest-model
      • can-fixture
      • can-fixture-socket
      • can-local-store
      • can-memory-store
      • can-ndjson-stream
      • can-query-logic
        • prototype
          • count
          • difference
          • filterMembers
          • filterMembersAndGetCount
          • identityKeys
          • index
          • intersection
          • isDefinedAndHasMembers
          • isEqual
          • isMember
          • isProperSubset
          • isSpecial
          • isSubset
          • union
          • unionMembers
        • query format
          • Query
          • Comparison Operators
        • static methods
          • defineComparison
          • set.difference
          • set.intersection
          • set.union
        • static types
          • EMPTY
          • KeysAnd
          • UNDEFINABLE
          • UNIVERSAL
          • UNKNOWABLE
          • makeEnum
      • can-realtime-rest-model
      • can-rest-model
      • can-set-legacy
      • can-super-model
    • Routing
      • can-deparam
      • can-param
      • can-route
      • can-route-hash
      • can-route-mock
      • can-route-pushstate
    • JS Utilities
      • can-assign
      • can-define-lazy-value
      • can-diff
      • can-globals
      • can-join-uris
      • can-key
      • can-key-tree
      • can-make-map
      • can-parse-uri
      • can-queues
      • can-string
      • can-string-to-any
    • DOM Utilities
      • can-ajax
      • can-attribute-encoder
      • can-child-nodes
      • can-control
      • can-dom-data
      • can-dom-events
      • can-dom-mutate
      • can-event-dom-enter
      • can-event-dom-radiochange
      • can-fragment
    • Data Validation
      • can-type
      • can-validate
      • can-validate-interface
      • can-validate-legacy
      • can-validate-validatejs
    • Typed Data
      • can-cid
      • can-construct
      • can-construct-super
      • can-data-types
      • can-namespace
      • can-reflect
      • can-reflect-dependencies
      • can-reflect-promise
      • can-types
    • Polyfills
      • can-symbol
      • can-vdom
    • Core
    • Infrastructure
      • can-global
      • can-test-helpers
    • Ecosystem
    • Legacy
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

can-query-logic

  • npm package badge
  • Star
  • Edit on GitHub

Perform data queries and compare queries against each other. Provides logic useful for data caching and real-time behavior.

new QueryLogic( [schemaOrType] [,options] )

The can-query-logic package exports a constructor function that builds query logic from:

  • an optional schema or type argument, and
  • an optional options argument used to convert alternate parameters to the expected Query format.

For example, the following builds query logic from a can-define/map/map:

import {DefineMap, QueryLogic} from "can";

const Todo = DefineMap.extend({
    id: {
        identity: true,
        type: "number"
    },
    name: "string",
    complete: "boolean"
});

const todoQueryLogic = new QueryLogic(Todo);

const filter = todoQueryLogic.filterMembers({
    filter: {
        complete: false
    },
    sort: "-name",
    page: {start: 0, end: 19}
},[
    {id: 1, name: "do dishes", complete: false},
    {id: 2, name: "mow lawn", complete: true},
    // ...
]);
console.log( filter ); //-> [{id: 1, name: "do dishes", complete: false}]

Once a query logic instance is created, it can be used to perform actions using queries. For example, the following might select 20 incomplete todos from a list of todos:

import {DefineMap, QueryLogic} from "can";

const Todo = DefineMap.extend({
    id: {
        identity: true,
        type: "number"
    },
    name: "string",
    complete: "boolean"
});

const todoQueryLogic = new QueryLogic(Todo);

const filter = todoQueryLogic.filterMembers({
    filter: {
        complete: false
    },
    sort: "-name",
    page: {start: 0, end: 19}
},[
    {id: 1, name: "do dishes", complete: false},
    {id: 2, name: "mow lawn", complete: true},
    // ...
]);
console.log( filter ); //-> [{id: 1, name: "do dishes", complete: false}]

By default can-query-logic supports queries represented by the Query format. It supports a variety of operators and options. It looks like:

import {QueryLogic} from "can";
import {Todo} from "//unpkg.com/can-demo-models@5";

const todoQueryLogic = new QueryLogic(Todo);
// Perform query logic:
const filter = todoQueryLogic.filterMembers({
  // Selects only the todos that match.
  filter: {
    complete: false
  },
  // Sort the results of the selection
  sort: "-name",
  // Selects a range of the sorted result
  page: {start: 0, end: 19}
},[
  {id: 1, name: "do dishes", complete: false},
  {id: 2, name: "mow lawn", complete: true},
  // ...
]);
console.log( filter ); //-> [{id: 1, name: "do dishes", complete: false}]

Parameters

  1. schemaOrType {function(options)|can-reflect/schema}:

    Defines the behavior of keys on a Query. This is done with either:

    • A constructor function that supports can-reflect.getSchema. Currently, can-define/map/map supports the can.getSchema symbol:
    import {DefineMap, QueryLogic} from "can";
    
    const Todo = DefineMap.extend({
        id: {
            identity: true,
            type: "number"
        },
        name: "string",
        complete: "boolean"
    });
    
    const todoQueryLogic = new QueryLogic(Todo);
    
    const filter = todoQueryLogic.filterMembers({
        filter: {
            complete: false
        },
        sort: "-name",
        page: {start: 0, end: 19}
    },[
        {id: 1, name: "do dishes", complete: false},
        {id: 2, name: "mow lawn", complete: true},
        // ...
    ]);
    console.log( filter ); //-> [{id: 1, name: "do dishes", complete: false}]
    
    
    • A schema object that looks like the following:

      import {QueryLogic, MaybeNumber, MaybeString, MaybeBoolean} from "can";
      
      const queryLogic = new QueryLogic({
        // keys that uniquely represent this type
        identity: ["id"],
        keys: {
          id: MaybeNumber,
          name: MaybeString,
          complete: MaybeBoolean
        }
      });
      
      const result = queryLogic.filterMembers({ filter: {complete: false}}, [
        {id: "1", name: "Justin", complete: "truthy"},
        {id: "2", name: "Paula", complete: ""},
        {id: "3", name: "Kevin", complete: true}
      ]);
      
      console.log( result );
      

      Note that if a key type (ex: name: MaybeString) is not provided, filtering by that key will still work, but there won't be any type coercion. For example, the following might not produce the desired results:

      import {QueryLogic} from "can";
      
      const queryLogic = new QueryLogic();
      const unionized = queryLogic.union(
        {filter: {age: 7}},
        {filter: {age: "07"}}
      );
      console.log( JSON.stringify( unionized ) ); //-> "{'filter':{'age':{'$in':[7,'07']}}}"
      

    Use types like can-data-types/maybe-number/maybe-number if you want to add basic type coercion:

    import {QueryLogic, MaybeNumber} from "can";
    
    const queryLogic = new QueryLogic({
      identity: ["id"],
      keys: {age: MaybeNumber}
    });
    const unionized = queryLogic.union(
      {filter: {age: 7}},
      {filter: {age: "07"}}
    );
    console.log( JSON.stringify( unionized ) ); //-> {filter: {age: 7}}
    

    If you need even more special key behavior, read defining properties with special logic.

    By default, filter properties like status in {filter: {status: "complete"}} are used to create to one of the Comparison Operators like GreaterThan. A matching schema key will overwrite this behavior. How this works is explained in the Defining filter properties with special logic section below.

Purpose

can-query-logic is used to give CanJS an understanding of what the parameters used to retrieve a list of data represent. This awareness helps other libraries like can-connect and can-fixture provide real-time, caching and other behaviors.

The parameters used to retrieve a list of data?

In many applications, you request a list of data by making a fetch or XMLHTTPRequest to a url like:

/api/todos?filter[complete]=true&sort=name

The values after the ? are used to control the data that comes back. Those values are deserialized into a query object look like this:

{
  filter: {complete: true},
  sort: "name"
}

This object represents a Query. This specific query is for requesting completed todos and have the todos sorted by their name.

A QueryLogic instance understands what a Query represents. For example, it can filter records that match a particular query:

import {QueryLogic} from "can";

const todos = [
  { id: 1, name: "learn CanJS",   complete: true  },
  { id: 2, name: "wash the car",  complete: false },
  { id: 3, name: "do the dishes", complete: true  }
];

const queryLogic = new QueryLogic();

const result = queryLogic.filterMembers({
  filter: {complete: true},
  sort: "name",
}, todos);

console.log( result ); //-> [
//  { id: 3, name: "do the dishes", complete: true  },
//  { id: 1, name: "learn CanJS",   complete: true  }
//]

The filterMembers method allows QueryLogic to be used similar to a database. QueryLogic instances methods help solve other problems too:

  • real-time - isMember returns if a particular item belongs to a query and index returns the location where that item belongs.
  • caching - isSubset can tell you if you've already loaded data you are looking for. difference can tell you what data you need to load that already isn't in your cache.

In fact, can-query-logic's most unique ability is to be able to directly compare queries that represent sets of data instead of having to compare the data itself. For example, if you already loaded all completed todos, can-query-logic can tell you how to get all remaining todos:

import {QueryLogic} from "can";

const completedTodosQuery = {filter: {complete: false}};
const allTodosQuery = {};

const queryLogic = new QueryLogic();
const remainingTodosQuery = queryLogic.difference(allTodosQuery, completedTodosQuery);

console.log( JSON.stringify( remainingTodosQuery ) ); //-> "{'filter':{'complete':{'$ne':false}}}"

Use

There are two main uses of can-query-logic:

  • Configuring a QueryLogic instance to match your service behavior.
  • Using a QueryLogic instance to create a new can-connect behavior.

Configuration

Most people will only ever need to configure a QueryLogic logic instance. Once properly configured, all can-connect behaviors will work correctly. If your service parameters match the default query structure, you likely don't need to use can-query-logic directly at all. However, if your service parameters differ from the default query structure or they need additional logic, some configuration will be necessary.

Matching the default query structure

By default, can-query-logic assumes your service layer will match a default query structure that looks like:

import {QueryLogic} from "can";

const queryLogic = new QueryLogic()

const filter = queryLogic.filterMembers({
  // Selects only the todos that match.
  filter: {
    complete: {$in: [false, null]}
  },
  // Sort the results of the selection
  sort: "-name",
  // Selects a range of the sorted result
  page: {start: 0, end: 19}
},
[
  {id: 1, name: "do dishes", complete: false},
  {id: 2, name: "mow lawn", complete: true},
  // ...
]);

console.log( filter ); //-> [{id: 1, name: "do dishes", complete: false}]

This structures follows the Fetching Data JSONAPI specification.

There's:

  • a filter property for filtering records,
  • a sort property for specifying the order to sort records, and
  • a page property that selects a range of the sorted result. The range indexes are inclusive.

NOTE: can-connect does not follow the rest of the JSONAPI specification. Specifically can-connect expects your server to send back JSON data in a different format.

If you control the service layer, we encourage you to make it match the default Query. The default query structure also supports the following Comparison Operators: $eq, $gt, $gte, $in, $lt, $lte, $ne, $nin.

If you support the default structure, it's very likely the entire configuration you need to perform will happen on the data type you pass to your can-connect connection. For example, you might create a Todo data type and pass it to a connection like this:

import {DefineMap, DefineList, realtimeRestModel} from "can";
import {Todo, todoFixture} from "//unpkg.com/can-demo-models@5";

// creates a mock todo api
todoFixture(1);

Todo.List = DefineList.extend("TodoList", {
  "#": {Type: Todo}
});

Todo.connection = realtimeRestModel({
  url: "/api/todos/{id}",
  Map: Todo
});

Todo.getList().then(todos => {
  todos.forEach(todo => {
    console.log(todo.name); // logs todos
  });
});

Internally, realTimeRest is using Todo to create and configure a QueryLogic instance for you. The previous example is equivalent to:

import {DefineMap, DefineList, realtimeRestModel, QueryLogic} from "can";
import {Todo, todoFixture} from "//unpkg.com/can-demo-models@5";

// creates a mock todo api
todoFixture(1);

Todo.List = DefineList.extend("TodoList", {
  "#": {Type: Todo}
});

const todoQueryLogic = new QueryLogic(Todo);

Todo.connection = realtimeRestModel({
  url: "/api/todos/{id}",
  Map: Todo,
  queryLogic: todoQueryLogic
});

Todo.getList().then(todos => {
  todos.forEach(todo => {
    console.log(todo.name); // logs todos
  });
});

If your services don't match the default query structure or logic, read on to see how to configure your query to match your service layer.

Changing the query structure

If the logic of your service layer matches the logic of the default query, but the form of the query parameters is different, the easiest way to configure the QueryLogic is to translate your parameter structure to the default query structure.

For example, to change queries to use where instead of filter so that queries can be made like:

import {DefineMap, DefineList, realtimeRestModel, QueryLogic} from "can";
import {Todo, todoFixture} from "//unpkg.com/can-demo-models@5";

// creates a mock todo api
todoFixture(5);

Todo.List = DefineList.extend("TodoList", {
  "#": {Type: Todo}
});

const todoQueryLogic = new QueryLogic(Todo);

Todo.connection = realtimeRestModel({
  url: "/api/todos/{id}",
  Map: Todo,
});

Todo.getList({filter: {complete: true}}).then(todos => {
  todos.forEach(todo => {
    console.log(todo.name); // logs completed todos
  });
});

You can use the options' toQuery and toParams functions to set the filter property value to the passed in where property value.

import {DefineMap, QueryLogic, realtimeRestModel} from "can";
import {Todo, todoFixture} from "//unpkg.com/can-demo-models@5";

todoFixture(5);

// CREATE YOUR QUERY LOGIC
const todoQueryLogic = new QueryLogic(Todo, {
  // Takes what your service expects: {where: {...}}
  // Returns what QueryLogic expects: {filter: {...}}
  toQuery(params){
    const where = params.where;
    delete params.where;
    params.filter = where;
    return params;
  },
  // Takes what QueryLogic expects: {filter: {...}}
  // Returns what your service expects: {where: {...}}
  toParams(query){
    const where = query.filter;
    delete query.filter;
    query.where = where;
    return query;
  }
});

Todo.List = DefineList.extend("TodoList", {
  "#": {Type: Todo}
});

// PASS YOUR QueryLogic TO YOUR CONNECTION
Todo.connection = realtimeRestModel({
  url: "/api/todos/{id}",
  Map: Todo,
  queryLogic: todoQueryLogic
});

Todo.getList({filter: {complete:true}}).then(todos => {
  todos.forEach(todo => {
    console.log(todo.name); // shows FILTERED todos
  });
});

Defining filter properties with special logic

If the logic of the default query is not adequate to represent the behavior of your service layer queries, you can define special classes called SetTypes to provide the additional logic.

Depending on your needs, this can be quite complex or rather simple. The following sections provide configuration examples in increasing complexity.

Before reading the following sections, it's useful to have some background information on how can-query-logic works. We suggest reading the How it works section.

Built-in special types

can-query-logic comes with functionality that can be used to create special logic. For example, the makeEnum method can be used to build a Status type that contains ONLY the enumerated values:

import {QueryLogic, DefineMap} from "can";

const Status = QueryLogic.makeEnum(["new","assigned","complete"]);

const Todo = DefineMap.extend({
  id: "number",
  status: Status,
  complete: "boolean",
  name: "string"
});

const todoLogic = new QueryLogic(Todo);
const unionQuery = todoLogic.union(
  {filter: {status: ["new","assigned"] }},
  {filter: {status: "complete" }}
)

console.log( unionQuery ); //-> {}

NOTE: unionQuery is empty because if we loaded all todos that are new, assigned, and complete, we've loaded every todo.
The {} query would load every todo.

Custom types that work with the comparison operators

If a number or string can represent your type, then you can create a SetType class that can be used with the comparison operators.

The SetType needs to be able to translate back and forth from the values in the query to a number or string.

For example, you might want to represent a date with a string like:

{
    filter: {date: {$gt: "Wed Apr 04 2018 10:00:00 GMT-0500 (CDT)"}}
}

The following creates a DateStringSet that translates a date string to a number:

import {DefineMap, QueryLogic} from "can";

class DateStringSet {
    constructor(value){
        this.value = value;
    }
    // used to convert to a number
    valueOf(){
        return new Date(this.value).getTime();
    }
    [Symbol.for("can.serialize")](){
        return this.value;
    }
}

const DateString = {
    [Symbol.for("can.new")]: function(v){ return v; },
    [Symbol.for("can.SetType")]: DateStringSet
};

const Todo = DefineMap.extend({
    id: {type: "number", identity: true},
    name: "string",
    date: DateString
});

const queryLogic = new QueryLogic(Todo);

const filter = queryLogic.filterMembers(
    {filter: {date: {$gt: "Wed Apr 04 2018 10:00:00 GMT-0500 (CDT)"}}},
    [{id: 1, name: "Learn CanJS", date: "Thurs Apr 05 2017 10:00:00 GMT-0500 (CDT)"},
    {id: 2, name: "grab coffee", date: "Wed Apr 03 2018 10:00:00 GMT-0500 (CDT)"},
    {id: 3, name: "finish these docs", date: "Thurs Apr 05 2018 10:00:00 GMT-0500 (CDT)"}]
);

console.log(filter); //-> [{
//  id: 2,
//  name: "finish these docs",
//  date: "Wed Apr 05 2018 10:00:00 GMT-0500 (CDT)"
// }]

These classes must provide:

  • constructor - initialized with the the value passed to a comparator (ex: "Wed Apr 04 2018 10:00:00 GMT-0500 (CDT)").
  • valueOf - return a string or number used to compare (ex: 1522854000000).
  • Symbol.for("can.serialize") - returns a string or number to compare against can-data-types for the query.

To configure a QueryLogic to use a SetType, it must be the can.SetType property on a schema's keys object. This can be done directly like:

new QueryLogic({
  keys: {
    date: {[Symbol.for("can.SetType")]: DateStringSet}
  }
});

More commonly, DateStringSet is the can.SetType symbol of a type like:

import {DefineMap, QueryLogic} from "can";

class DateStringSet {
    constructor(value){
        this.value = value;
    }
    // used to convert to a number
    valueOf(){
        return new Date(this.value).getTime();
    }
    [Symbol.for("can.serialize")](){
        return this.value;
    }
}

const DateString = {
    [Symbol.for("can.new")]: function(v){ return v; },
    [Symbol.for("can.SetType")]: DateStringSet
};

const Todo = DefineMap.extend({
    id: {type: "number", identity: true},
    name: "string",
    date: DateString
});

const queryLogic = new QueryLogic(Todo);

const filter = queryLogic.filterMembers(
    {filter: {date: {$gt: "Wed Apr 04 2018 10:00:00 GMT-0500 (CDT)"}}},
    [{id: 1, name: "Learn CanJS", date: "Thurs Apr 05 2017 10:00:00 GMT-0500 (CDT)"},
    {id: 2, name: "grab coffee", date: "Wed Apr 03 2018 10:00:00 GMT-0500 (CDT)"},
    {id: 3, name: "finish these docs", date: "Thurs Apr 05 2018 10:00:00 GMT-0500 (CDT)"}]
);

console.log(filter); //-> [{
//  id: 2,
//  name: "finish these docs",
//  date: "Wed Apr 05 2018 10:00:00 GMT-0500 (CDT)"
// }]

Then this DateString is used to configure your data type like:

import {DefineMap, QueryLogic} from "can";

class DateStringSet {
    constructor(value){
        this.value = value;
    }
    // used to convert to a number
    valueOf(){
        return new Date(this.value).getTime();
    }
    [Symbol.for("can.serialize")](){
        return this.value;
    }
}

const DateString = {
    [Symbol.for("can.new")]: function(v){ return v; },
    [Symbol.for("can.SetType")]: DateStringSet
};

const Todo = DefineMap.extend({
    id: {type: "number", identity: true},
    name: "string",
    date: DateString
});

const queryLogic = new QueryLogic(Todo);

const filter = queryLogic.filterMembers(
    {filter: {date: {$gt: "Wed Apr 04 2018 10:00:00 GMT-0500 (CDT)"}}},
    [{id: 1, name: "Learn CanJS", date: "Thurs Apr 05 2017 10:00:00 GMT-0500 (CDT)"},
    {id: 2, name: "grab coffee", date: "Wed Apr 03 2018 10:00:00 GMT-0500 (CDT)"},
    {id: 3, name: "finish these docs", date: "Thurs Apr 05 2018 10:00:00 GMT-0500 (CDT)"}]
);

console.log(filter); //-> [{
//  id: 2,
//  name: "finish these docs",
//  date: "Wed Apr 05 2018 10:00:00 GMT-0500 (CDT)"
// }]

NOTE: Types like DateString need to be distinguished from SetTypes like DateStringSet because types like DateString have different values. For example, a DateStringSet might have a value like "yesterday", but this would not be a valid DateString.

Completely custom types

If you want total control over filtering logic, you can create a SetType that provides the following:

  • methods:
    • can.isMember - A function that returns if an object belongs to the query.
    • can.serialize - A function that returns the serialized form of the type for the query.
  • comparisons:
    • union - The result of taking a union of two SetTypes.
    • intersection - The result of taking an intersection of two SetTypes.
    • difference - The result of taking a difference of two SetTypes.

The following creates a SearchableStringSet that is able to perform searches that match the provided text like:

import {QueryLogic} from "can";

const recipes = [
  {id: 1, name: "garlic chicken"},
  {id: 2, name: "ice cream"},
  {id: 3, name: "chicken kiev"}
];

const queryLogic = new QueryLogic();
const result = queryLogic.filterMembers({
  filter: {name: "chicken"}
}, recipes);

console.log( result ); //-> []

Notice how all values that match chicken are returned.

import {canReflect, QueryLogic} from "can";

// Takes the value of `name` (ex: `"chicken"`)
function SearchableStringSet(value) {
  this.value = value;
}

canReflect.assignSymbols(SearchableStringSet.prototype,{
  // Returns if the name on a todo is actually a member of the set.
  "can.isMember": function(value){
    return value.includes(this.value);
  },
  // Converts back to a value that can be in a query.
  "can.serialize": function(){
    return this.value;
  }
});

// Specify how to do the fundamental set comparisons.
QueryLogic.defineComparison(SearchableStringSet,SearchableStringSet,{
  // Return a set that would load all records in searchA and searchB.
  union(searchA, searchB){
    // If searchA's text contains searchB's text, then
    // searchB will include searchA's results.
    if(searchA.value.includes(searchB.value)) {
      // A:`food` ∪ B:`foo` => `foo`
      return searchB;
    }
    if(searchB.value.includes(searchA.value)) {
      // A:`foo` ∪ B:`food` => `foo`
      return searchA;
    }
    // A:`ice` ∪ B:`cream` => `ice` || `cream`
    return new QueryLogic.ValueOr([searchA, searchB]);
  },
  // Return a set that would load records shared by searchA and searchB.
  intersection(searchA, searchB){
    // If searchA's text contains searchB's text, then
    // searchA is the shared search results.
    if(searchA.value.includes(searchB.value)) {
        // A:`food` ∩ B:`foo` => `food`
        return searchA;
    }
    if(searchB.value.includes(searchA.value)) {
        // A:`foo` ∩ B:`food` => `food`
      return searchB;
    }
    // A:`ice` ∩ B:`cream` => `ice` && `cream`
    // But suppose AND isn't supported,
    // So we return `UNDEFINABLE`.
    return QueryLogic.UNDEFINABLE;
  },
  // Return a set that would load records in searchA that are not in
  // searchB.
  difference(searchA, searchB){
    // if searchA's text contains searchB's text, then
    // searchA has nothing outside what searchB would return.
    if(searchA.value.includes(searchB.value)) {
      // A:`food` \ B:`foo` => ∅
      return QueryLogic.EMPTY;
    }
    // If searchA has results outside searchB's results
    // then there are records, but we aren't able to
    // create a string that represents this.
    if(searchB.value.includes(searchA.value)) {
      // A:`foo` \ B:`food` => UNDEFINABLE
      return QueryLogic.UNDEFINABLE;
    }

    // A:`ice` \ B:`cream` => `ice` && !`cream`
    // If there's another situation, we
    // aren't able to express the difference
    // so we return UNDEFINABLE.
    return QueryLogic.UNDEFINABLE;
  }
});

const recipes = [
  {id: 1, name: "garlic chicken"},
  {id: 2, name: "ice cream"},
  {id: 3, name: "chicken kiev"}
];

const queryLogic = new QueryLogic({ keys: {
  name : {[Symbol.for("can.SetType")]: SearchableStringSet}
}});

const result = queryLogic.filterMembers({
  filter: {name: "chicken"}
}, recipes);

console.log( result ); //-> [
  // {id: 1, name: "garlic chicken"},
  // {id: 3, name: "chicken kiev"}
  // ]

To configure a QueryLogic to use a SetType, it must be the can.SetType property on a schema's keys object. This can be done directly like:

import {canReflect, QueryLogic} from "can";

// Takes the value of `name` (ex: `"chicken"`)
function SearchableStringSet(value) {
  this.value = value;
}

canReflect.assignSymbols(SearchableStringSet.prototype,{
  // Returns if the name on a todo is actually a member of the set.
  "can.isMember": function(value){
    return value.includes(this.value);
  },
  // Converts back to a value that can be in a query.
  "can.serialize": function(){
    return this.value;
  }
});

// Specify how to do the fundamental set comparisons.
QueryLogic.defineComparison(SearchableStringSet,SearchableStringSet,{
  // Return a set that would load all records in searchA and searchB.
  union(searchA, searchB){
    // If searchA's text contains searchB's text, then
    // searchB will include searchA's results.
    if(searchA.value.includes(searchB.value)) {
      // A:`food` ∪ B:`foo` => `foo`
      return searchB;
    }
    if(searchB.value.includes(searchA.value)) {
      // A:`foo` ∪ B:`food` => `foo`
      return searchA;
    }
    // A:`ice` ∪ B:`cream` => `ice` || `cream`
    return new QueryLogic.ValueOr([searchA, searchB]);
  },
  // Return a set that would load records shared by searchA and searchB.
  intersection(searchA, searchB){
    // If searchA's text contains searchB's text, then
    // searchA is the shared search results.
    if(searchA.value.includes(searchB.value)) {
        // A:`food` ∩ B:`foo` => `food`
        return searchA;
    }
    if(searchB.value.includes(searchA.value)) {
        // A:`foo` ∩ B:`food` => `food`
      return searchB;
    }
    // A:`ice` ∩ B:`cream` => `ice` && `cream`
    // But suppose AND isn't supported,
    // So we return `UNDEFINABLE`.
    return QueryLogic.UNDEFINABLE;
  },
  // Return a set that would load records in searchA that are not in
  // searchB.
  difference(searchA, searchB){
    // if searchA's text contains searchB's text, then
    // searchA has nothing outside what searchB would return.
    if(searchA.value.includes(searchB.value)) {
      // A:`food` \ B:`foo` => ∅
      return QueryLogic.EMPTY;
    }
    // If searchA has results outside searchB's results
    // then there are records, but we aren't able to
    // create a string that represents this.
    if(searchB.value.includes(searchA.value)) {
      // A:`foo` \ B:`food` => UNDEFINABLE
      return QueryLogic.UNDEFINABLE;
    }

    // A:`ice` \ B:`cream` => `ice` && !`cream`
    // If there's another situation, we
    // aren't able to express the difference
    // so we return UNDEFINABLE.
    return QueryLogic.UNDEFINABLE;
  }
});

const recipes = [
  {id: 1, name: "garlic chicken"},
  {id: 2, name: "ice cream"},
  {id: 3, name: "chicken kiev"}
];

const queryLogic = new QueryLogic({ keys: {
  name : {[Symbol.for("can.SetType")]: SearchableStringSet}
}});

const result = queryLogic.filterMembers({
  filter: {name: "chicken"}
}, recipes);

console.log( result ); //-> [
  // {id: 1, name: "garlic chicken"},
  // {id: 3, name: "chicken kiev"}
  // ]

More commonly, SearchableStringSet is the can.SetType symbol of a type like:

import {canReflect, DefineMap, QueryLogic} from "can";

// Takes the value of `name` (ex: `"chicken"`)
function SearchableStringSet(value) {
  this.value = value;
}

canReflect.assignSymbols(SearchableStringSet.prototype,{
  // Returns if the name on a todo is actually a member of the set.
  "can.isMember": function(value){
    return value.includes(this.value);
  },
  // Converts back to a value that can be in a query.
  "can.serialize": function(){
    return this.value;
  }
});

// Specify how to do the fundamental set comparisons.
QueryLogic.defineComparison(SearchableStringSet,SearchableStringSet,{
  // Return a set that would load all records in searchA and searchB.
  union(searchA, searchB){
    // If searchA's text contains searchB's text, then
    // searchB will include searchA's results.
    if(searchA.value.includes(searchB.value)) {
      // A:`food` ∪ B:`foo` => `foo`
      return searchB;
    }
    if(searchB.value.includes(searchA.value)) {
      // A:`foo` ∪ B:`food` => `foo`
      return searchA;
    }
    // A:`ice` ∪ B:`cream` => `ice` || `cream`
    return new QueryLogic.ValueOr([searchA, searchB]);
  },
  // Return a set that would load records shared by searchA and searchB.
  intersection(searchA, searchB){
    // If searchA's text contains searchB's text, then
    // searchA is the shared search results.
    if(searchA.value.includes(searchB.value)) {
        // A:`food` ∩ B:`foo` => `food`
        return searchA;
    }
    if(searchB.value.includes(searchA.value)) {
        // A:`foo` ∩ B:`food` => `food`
      return searchB;
    }
    // A:`ice` ∩ B:`cream` => `ice` && `cream`
    // But suppose AND isn't supported,
    // So we return `UNDEFINABLE`.
    return QueryLogic.UNDEFINABLE;
  },
  // Return a set that would load records in searchA that are not in
  // searchB.
  difference(searchA, searchB){
    // if searchA's text contains searchB's text, then
    // searchA has nothing outside what searchB would return.
    if(searchA.value.includes(searchB.value)) {
      // A:`food` \ B:`foo` => ∅
      return QueryLogic.EMPTY;
    }
    // If searchA has results outside searchB's results
    // then there are records, but we aren't able to
    // create a string that represents this.
    if(searchB.value.includes(searchA.value)) {
      // A:`foo` \ B:`food` => UNDEFINABLE
      return QueryLogic.UNDEFINABLE;
    }

    // A:`ice` \ B:`cream` => `ice` && !`cream`
    // If there's another situation, we
    // aren't able to express the difference
    // so we return UNDEFINABLE.
    return QueryLogic.UNDEFINABLE;
  }
});

const SearchableString = {
  [Symbol.for("can.SetType")]: SearchableStringSet
};

const Todo = DefineMap.extend({
  id: {type: "number", identity: true},
  name: SearchableString,
});

const todos = [
  {id: 1, name: "important meeting"},
  {id: 2, name: "fall asleep during meeting"},
  {id: 3, name: "find out what important means"}
];

const queryLogic = new QueryLogic(Todo);

const result = queryLogic.filterMembers({
  filter: {name: "important"}
}, todos);

console.log( result ); //->[{id: 1, name: "important meeting"},{id: 3, name: "find out what important means"}]

Then this SearchableString is used to configure your data type like:

import {canReflect, DefineMap, QueryLogic} from "can";

// Takes the value of `name` (ex: `"chicken"`)
function SearchableStringSet(value) {
  this.value = value;
}

canReflect.assignSymbols(SearchableStringSet.prototype,{
  // Returns if the name on a todo is actually a member of the set.
  "can.isMember": function(value){
    return value.includes(this.value);
  },
  // Converts back to a value that can be in a query.
  "can.serialize": function(){
    return this.value;
  }
});

// Specify how to do the fundamental set comparisons.
QueryLogic.defineComparison(SearchableStringSet,SearchableStringSet,{
  // Return a set that would load all records in searchA and searchB.
  union(searchA, searchB){
    // If searchA's text contains searchB's text, then
    // searchB will include searchA's results.
    if(searchA.value.includes(searchB.value)) {
      // A:`food` ∪ B:`foo` => `foo`
      return searchB;
    }
    if(searchB.value.includes(searchA.value)) {
      // A:`foo` ∪ B:`food` => `foo`
      return searchA;
    }
    // A:`ice` ∪ B:`cream` => `ice` || `cream`
    return new QueryLogic.ValueOr([searchA, searchB]);
  },
  // Return a set that would load records shared by searchA and searchB.
  intersection(searchA, searchB){
    // If searchA's text contains searchB's text, then
    // searchA is the shared search results.
    if(searchA.value.includes(searchB.value)) {
        // A:`food` ∩ B:`foo` => `food`
        return searchA;
    }
    if(searchB.value.includes(searchA.value)) {
        // A:`foo` ∩ B:`food` => `food`
      return searchB;
    }
    // A:`ice` ∩ B:`cream` => `ice` && `cream`
    // But suppose AND isn't supported,
    // So we return `UNDEFINABLE`.
    return QueryLogic.UNDEFINABLE;
  },
  // Return a set that would load records in searchA that are not in
  // searchB.
  difference(searchA, searchB){
    // if searchA's text contains searchB's text, then
    // searchA has nothing outside what searchB would return.
    if(searchA.value.includes(searchB.value)) {
      // A:`food` \ B:`foo` => ∅
      return QueryLogic.EMPTY;
    }
    // If searchA has results outside searchB's results
    // then there are records, but we aren't able to
    // create a string that represents this.
    if(searchB.value.includes(searchA.value)) {
      // A:`foo` \ B:`food` => UNDEFINABLE
      return QueryLogic.UNDEFINABLE;
    }

    // A:`ice` \ B:`cream` => `ice` && !`cream`
    // If there's another situation, we
    // aren't able to express the difference
    // so we return UNDEFINABLE.
    return QueryLogic.UNDEFINABLE;
  }
});

const SearchableString = {
  [Symbol.for("can.SetType")]: SearchableStringSet
};

const Todo = DefineMap.extend({
  id: {type: "number", identity: true},
  name: SearchableString,
});

const todos = [
  {id: 1, name: "important meeting"},
  {id: 2, name: "fall asleep during meeting"},
  {id: 3, name: "find out what important means"}
];

const queryLogic = new QueryLogic(Todo);

const result = queryLogic.filterMembers({
  filter: {name: "important"}
}, todos);

console.log( result ); //->[{id: 1, name: "important meeting"},{id: 3, name: "find out what important means"}]

NOTE: Types like SearchableString need to be distinguished from SetTypes like SearchableStringSet because types like SearchableString have different values. For example, a SearchableStringSet might have a value like "yesterday", but this would not be a valid SearchableString.

Testing your QueryLogic

It can be very useful to test your QueryLogic before using it with can-connect.

import {DefineMap, QueryLogic} from "can";

const Todo = DefineMap.extend({ ... });

const queryLogic = new QueryLogic(Todo, {
  toQuery(params){ ... },
  toParams(query){ ... }
});

unit.test("isMember", function(){
  const result = queryLogic.isMember({
    filter: {special: "SOMETHING SPECIAL"}
  },{
    id: 0,
    name: "I'm very special"
  });
  assert.ok(result, "is member");
});

How it works

The following gives a rough overview of how can-query-logic works:

1. Types are defined:

A user defines the type of data that will be loaded from the server:

import {DefineMap, QueryLogic} from "can";

const Todo = DefineMap.extend({
    id: {
        identity: true,
        type: "number"
    },
    name: "string",
    complete: "boolean"
});

const todoQueryLogic = new QueryLogic(Todo);

const unionization = todoQueryLogic.union(
  { filter: {name: "assigned"} },
  { filter: {name: "complete"} }
);

console.log( JSON.stringify(unionization) ); //-> "{'filter':{'name':{'$in':['assigned','complete']}}}"

2. The defined type exposes a schema:

can-define/map/maps expose this type information as a schema:

import {DefineMap, QueryLogic} from "can";

const Todo = DefineMap.extend({
    id: {
        identity: true,
        type: "number"
    },
    name: "string",
    complete: "boolean"
});

const todoQueryLogic = new QueryLogic(Todo);

const unionization = todoQueryLogic.union(
  { filter: {name: "assigned"} },
  { filter: {name: "complete"} }
);

console.log( JSON.stringify(unionization) ); //-> "{'filter':{'name':{'$in':['assigned','complete']}}}"

3. The schema is used by can-query-logic to create set instances:

When a call to .filter() happens like:

import {DefineMap, QueryLogic} from "can";

const Todo = DefineMap.extend({
    id: {
        identity: true,
        type: "number"
    },
    name: "string",
    complete: "boolean"
});

const todoQueryLogic = new QueryLogic(Todo);

const unionization = todoQueryLogic.union(
  { filter: {name: "assigned"} },
  { filter: {name: "complete"} }
);

console.log( JSON.stringify(unionization) ); //-> "{'filter':{'name':{'$in':['assigned','complete']}}}"

The queries (ex: { filter: {name: "assigned"} }) are hydrated to SetTypes like:

const assignedSet = new BasicQuery({
  filter: new And({
    name: new Status[Symbol.for("can.SetType")]("assigned")
  })
});

NOTE: hydrated is the opposite of serialization. It means we take a plain JavaScript object like { filter: {name: "assigned"} } and create instances of types with it.

The following is a more complex query and what it gets hydrated to:

import {canReflect, QueryLogic} from "can";
//query
const queryLogic = new QueryLogic({
  filter: {
    age: {$gt: 22}
  },
  sort: "-name",
  page: {start: 0, end: 9}
});

console.log( canReflect.getSchema(queryLogic) ); //-> {
//   filter: {
//     age: {$gt: 22}
//   },
//   sort: "-name",
//   page: {start: 0, end: 9}
// }
// hydrated set types
new BasicQuery({
  filter: new And({
    age: new GreaterThan(22)
  }),
  sort: "-name",
  page: new RealNumberRangeInclusive(0,9)
});

Once queries are hydrated, can-query/src/set is used to perform the union:

set.union(assignedSet, completeSet);

set.union looks for comparator functions specified on their constructor's can.setComparisons symbol property. For example, BasicQuery has a can.setComparisons property and value like the following:

import {BasicQuery} from "can";

BasicQuery[Symbol.for("can.setComparisons")] = new Map([
  [BasicQuery]: new Map([
    [BasicQuery]: {union, difference, intersection}
    [QueryLogic.UNIVERSAL]: {difference}
  ])
]);

Types like BasicQuery and And are "composer" types. Their union, difference and intersection methods perform union, difference and intersection on their children types.

can-query-logics methods reflect set theory operations. That's why most types need a union, intersection, and difference method. With that, other methods like isEqual and isSubset can be derived.

In this case, set.union will call BasicQuery's union with itself. This will see that the sort and page results match and simply return a new BasicQuery with the union of the filters:

new BasicQuery({
  filter: set.union( assignedSet.filter, completeSet.filter )
})

This will eventually result in a query like:

new BasicQuery({
  filter: new And({
    name: new Status[Symbol.for("can.SetType")]("assigned", "complete")
  })
})

4. The resulting query is serialized:

Finally, this set will be serialized to:

{
  filter: {
    name: ["assigned", "complete"]
  }
}

The serialized output above is what is returned as a result of the union.

Code Organization

On a high level, can-query-logic is organized in four places:

  • src/set.js - The core "set logic" functionality. For example set.isEqual is built to derive from using underlying difference and intersection operators.
  • src/types/* - These are the SetType constructors used to make comparisons between different sets or properties.
  • src/serializers/* - These provide hydration and serialization methods used to change the plain JavaScript query objects to SetTypes and back to plain JavaScript query objects.
  • can-query-logic.js - Assembles all the different types and serializers to hydrate a query object to a SetType instance, then uses set.js's logic to perform the set logic and serialize the result.

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 6.6.1.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news