filter.js

// *****************************************************************************
// Copyright 2013-2023 Aerospike, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License")
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// *****************************************************************************

'use strict'

const as = require('bindings')('aerospike.node')
const GeoJSON = require('./geojson')

/**
 * @module aerospike/filter
 *
 * @description This module provides functions to create secondary index (SI) filter
 * predicates for use in query operations via the {@link Client#query} command.
 *
 * @see {@link Query}
 *
 * @example
 *
 * const Aerospike = require('aerospike')
 * const Context = Aerospike.cdt.Context
 *
 * Aerospike.connect().then(async (client) => {
 *   // find any records that have a recent location within 1000m radius of the specified coordinates
 *   let geoFilter = Aerospike.filter.geoWithinRadius('recent', 103.8, 1.305, 1000, Aerospike.indexType.LIST, new Context().addListIndex(0))
 *   let query = client.query('test', 'demo')
 *   query.where(geoFilter)
 *
 *   let results = await query.results()
 *   for (let record in results) {
 *     console.log(record.bins.recent)
 *   }
 *   client.close()
 * })
 */

/**
 * @class module:aerospike/filter~SindexFilterPredicate
 * @classdesc SI filter predicate to limit the scope of a {@link Query}.
 *
 * Filter predicates must be instantiated using the methods in the {@link
 * module:aerospike/filter} module.
 */
class SindexFilterPredicate {
  constructor (predicate, bin, dataType, indexType, context, props) {
    this.predicate = predicate
    this.bin = bin
    this.datatype = dataType
    this.type = indexType || as.indexType.DEFAULT
    this.context = context
    if (props) {
      Object.assign(this, props)
    }
  }
}
exports.SindexFilterPredicate = SindexFilterPredicate

class EqualPredicate extends SindexFilterPredicate {
  constructor (bin, value, dataType, indexType, context) {
    super(as.predicates.EQUAL, bin, dataType, indexType, context, {
      val: value
    })
  }
}

class RangePredicate extends SindexFilterPredicate {
  constructor (bin, min, max, dataType, indexType, context) {
    super(as.predicates.RANGE, bin, dataType, indexType, context, {
      min,
      max
    })
  }
}

class GeoPredicate extends SindexFilterPredicate {
  constructor (bin, value, indexType, context) {
    super(as.predicates.RANGE, bin, as.indexDataType.GEO2DSPHERE, indexType, context, {
      val: value
    })
  }
}

// Helper function to determine the type of a primitive or Object
function typeOf (value) {
  if (value === null) return 'null'
  let valueType = typeof value
  if (valueType === 'object') {
    valueType = value.constructor.name.toLowerCase()
  }
  return valueType
}

function dataTypeOf (value) {
  switch (typeOf(value)) {
    case 'string':
      return as.indexDataType.STRING
    case 'number':
    case 'double':
      return as.indexDataType.NUMERIC
    default:
      if (Buffer.isBuffer(value)) {
        return as.indexDataType.BLOB
      }

      throw new TypeError('Unknown data type for filter value.')
  }
}

/**
 * @summary Integer range filter.
 * @description The filter matches records with a bin value in the given
 * integer range. The filter can also be used to match for integer values
 * within the given range that are contained with a list or map by specifying
 * the appropriate index type.
 *
 * @param {string} bin - The name of the bin.
 * @param {number} min - Lower end of the range (inclusive).
 * @param {number} max - Upper end of the range (inclusive).
 * @param {number} [indexType=Aerospike.indexType.DEFAULT] - One of {@link
 * module:aerospike.indexType}, i.e. LIST or MAPVALUES.
 * @param {Object} context - The {@link CdtContext} of the index.
 * @returns {module:aerospike/filter~SindexFilterPredicate} SI
 * filter predicate, that can be applied to queries using {@link Query#where}.
 */
exports.range = function (bin, min, max, indexType, context) {
  const dataType = as.indexDataType.NUMERIC
  return new RangePredicate(bin, min, max, dataType, indexType, context)
}

/**
 * @summary String/integer equality filter.
 * @description The filter matches records with a bin that matches a specified
 * string or integer value.
 *
 * @param {string} bin - The name of the bin.
 * @param {string} value - The filter value.
 * @returns {module:aerospike/filter~SindexFilterPredicate} SI
 * filter predicate, that can be applied to queries using {@link Query#where}.
 */
exports.equal = function (bin, value) {
  const dataType = dataTypeOf(value)
  return new EqualPredicate(bin, value, dataType)
}

/**
 * @summary Filter for list/map membership.
 * @description The filter matches records with a bin that has a list or map
 * value that contain the given string or integer.
 *
 * @param {string} bin - The name of the bin.
 * @param {(string|number)} value - The value that should be a member of the
 * list or map in the bin.
 * @param {number} indexType - One of {@link module:aerospike.indexType},
 * i.e. LIST, MAPVALUES or MAPKEYS.
 * @param {Object} context - The {@link CdtContext} of the index.
 * @returns {module:aerospike/filter~SindexFilterPredicate} SI
 * filter predicate, that can be applied to queries using {@link Query#where}.
 *
 * @since v2.0
 */
exports.contains = function (bin, value, indexType, context) {
  const dataType = dataTypeOf(value)
  return new EqualPredicate(bin, value, dataType, indexType, context)
}

/**
 * @summary Geospatial filter that matches points within a given GeoJSON
 * region.
 * @description Depending on the index type, the filter will match GeoJSON
 * values contained in list or map values as well (requires Aerospike server
 * version >= 3.8).
 *
 * @param {string} bin - The name of the bin.
 * @param {GeoJSON} value - GeoJSON region value.
 * @param {number} [indexType=Aerospike.indexType.DEFAULT] - One of {@link
 * module:aerospike.indexType}, i.e. LIST or MAPVALUES.
 * @param {Object} context - The {@link CdtContext} of the index.
 * @returns {module:aerospike/filter~SindexFilterPredicate} SI
 * filter predicate, that can be applied to queries using {@link Query#where}.
 *
 * @since v2.0
 */
exports.geoWithinGeoJSONRegion = function (bin, value, indexType, context) {
  if (value instanceof GeoJSON) {
    value = value.toString()
  } else if (typeof value === 'object') {
    value = JSON.stringify(value)
  }
  return new GeoPredicate(bin, value, indexType, context)
}

/**
 * @summary Geospatial filter that matches regions that contain a given GeoJSON
 * point.
 * @description Depending on the index type, the filter will match GeoJSON
 * regions within list or map values as well (requires server
 * >= 3.8).
 *
 * @param {string} bin - The name of the bin.
 * @param {GeoJSON} value - GeoJSON point value.
 * @param {number} [indexType=Aerospike.indexType.DEFAULT] - One of {@link
 * module:aerospike.indexType}, i.e. LIST or MAPVALUES.
 * @param {Object} context - The {@link CdtContext} of the index.
 * @returns {module:aerospike/filter~SindexFilterPredicate} SI
 * filter predicate, that can be applied to queries using {@link Query#where}.
 *
 * @since v2.0
 */
exports.geoContainsGeoJSONPoint = function (bin, value, indexType, context) {
  if (value instanceof GeoJSON) {
    value = value.toString()
  } else if (typeof value === 'object') {
    value = JSON.stringify(value)
  }
  return new GeoPredicate(bin, value, indexType, context)
}

/**
 * @summary Geospatial filter that matches points within a radius from a given
 * point.
 * @description Depending on the index type, the filter will match GeoJSON
 * values contained in list or map values as well (requires Aerospike server
 * version >= 3.8).
 *
 * @param {string} bin - The name of the bin.
 * @param {number} lng - Longitude of the center point.
 * @param {number} lat - Latitude of the center point.
 * @param {number} radius - Radius in meters.
 * @param {number} [indexType=Aerospike.indexType.DEFAULT] - One of {@link
 * module:aerospike.indexType}, i.e. LIST or MAPVALUES.
 * @param {Object} context - The {@link CdtContext} of the index.
 * @returns {module:aerospike/filter~SindexFilterPredicate} SI
 * filter predicate, that can be applied to queries using {@link Query#where}.
 *
 * @since v2.0
 */
exports.geoWithinRadius = function (bin, lon, lat, radius, indexType, context) {
  const circle = GeoJSON.Circle(lon, lat, radius)
  return new GeoPredicate(bin, circle.toString(), indexType, context)
}

/**
 * @summary Geospatial filter that matches regions that contain a given lng/lat
 * coordinate.
 * @description Depending on the index type, the filter will match GeoJSON
 * regions within list or map values as well (requires server
 * >= 3.8).
 *
 * @param {string} bin - The name of the bin.
 * @param {number} lng - Longitude of the point.
 * @param {number} lat - Latitude of the point.
 * @param {number} [indexType=Aerospike.indexType.DEFAULT] - One of {@link
 * module:aerospike.indexType}, i.e. LIST or MAPVALUES.
 * @param {Object} context - The {@link CdtContext} of the index.
 * @returns {module:aerospike/filter~SindexFilterPredicate} SI
 * filter predicate, that can be applied to queries using {@link Query#where}.
 *
 * @since v2.0
 */
exports.geoContainsPoint = function (bin, lon, lat, indexType, context) {
  const point = GeoJSON.Point(lon, lat)
  return new GeoPredicate(bin, point.toString(), indexType, context)
}