Skip to content

Repository files navigation

 ██████╗  █████╗ ██████╗
██╔═══██╗██╔══██╗██╔══██╗
██║   ██║███████║██████╔╝
██║▄▄ ██║██╔══██║██╔══██╗
╚██████╔╝██║  ██║██║  ██║
 ╚══▀▀═╝ ╚═╝  ╚═╝╚═╝  ╚═╝
-=| MongoDB-style queries for plain JavaScript arrays. |=-

GitHub License npm GitHub Repo stars Run tests codecov Sponsor

What is Qar?

MongoDB-style queries for plain JavaScript arrays. Simple, lightweight, and perfect for JS Apps, static sites, and JSON data.

Table of Contents

[Use Cases] [Why Qar?] [Installation] [Quick Start] [API Reference] [Query Operators] [Nested Properties] [Field Projection] [Aggregation Pipeline] [Index Optimization (Large Datasets)] [$skip Stage] [$lookup Stage (Join)] [Examples] [Advanced Examples] [Why Qar over alternatives?] [TypeScript] [Browser Support] [Guidelines] [License]

Sponsoring

If you find this project useful, please consider sponsoring me by:

[Donate via GitHub] [Donate via PayPal] [Give the repo a Star] [Follow me on GitHub] [Share the project on X]

Use Cases

  • Query static JSON data in modern JS apps
  • Next.js/React/Vue/Node developers working with static JSON data
  • Frontend developers who need pipeline analytics
  • Backend developers needing in-memory querying without a database
  • Filter and search arrays of objects without a database
  • Perform complex data transformations with aggregation pipelines

Why Qar?

When you have a JSON file or an array of objects and need to filter, search, or query them with MongoDB-style syntax - without setting up a database.

import Qar from 'qarjs';

const products = new Qar(productsData);

// Simple queries
products.find({ category: 'electronics', price: { $lt: 500 } }).toArray();

// Chainable API with sorting and pagination
products.find({ inStock: true }).sort({ price: -1 }).limit(10).toArray();

// Field projection
products.find({ category: 'phones' }, { name: 1, price: 1, _id: 0 }).toArray();

// Aggregation pipeline
products.aggregate([
  { $match: { inStock: true } },
  { $group: { _id: '$category', count: { $sum: 1 } } },
  { $sort: { count: -1 } },
]);

// Get unique values
products.distinct('category'); // => ['electronics', 'clothing', 'books']

Installation

npm install qarjs
yarn add qarjs
pnpm add qarjs

Quick Start

import Qar from 'qarjs';

const data = [
  { _id: 1, name: 'Alice', age: 20, role: 'user' },
  { _id: 2, name: 'Bob', age: 30, role: 'admin' },
  { _id: 3, name: 'Carol', age: 40, role: 'user' },
];

const users = new Qar(data);

// Find all users older than 25
users.find({ age: { $gt: 25 } }).toArray();
// => [{ _id: 2, ... }, { _id: 3, ... }]

// Find one admin
users.findOne({ role: 'admin' });
// => { _id: 2, name: 'Bob', age: 30, role: 'admin' }

// Count users
users.count({ role: 'user' });
// => 2

// Check if exists
users.exists({ name: 'Alice' });
// => true

API Reference

Constructor

const collection = new Qar(arrayOfObjects);

Creates a new Qar instance with your array of objects.

Methods

find(query, projection?, options?)

Returns a cursor for chaining operations. Call .toArray() to execute.

// Basic query
users.find({ age: { $gte: 30 } }).toArray();

// With projection
users.find({ role: 'admin' }, { name: 1, email: 1 }).toArray();

// With index hints for performance (large datasets)
const index = new Map();
index.set('electronics', [...electronicsProducts]);
users.find({ category: 'electronics' }, {}, { indexes: { category: index } }).toArray();

// Chainable
users.find({ active: true }).sort({ name: 1 }).skip(10).limit(5).toArray();

findOne(query, projection?, options?)

Returns the first object matching the query, or null if not found. Supports the same options as find().

users.findOne({ name: 'Alice' });
// => { _id: 1, name: 'Alice', ... } or null

users.findOne({ role: 'admin' }, { name: 1, email: 1 });
// => { name: 'Bob', email: 'bob@example.com' }

// With index hint
users.findOne({ category: 'electronics' }, {}, { indexes: { category: index } });

count(query, options?)

Returns the number of objects matching the query.

users.count({ role: 'admin' });
// => 1

// With index hint for performance
const index = new Map();
index.set('electronics', [...electronicsProducts]);
users.count({ category: 'electronics' }, { indexes: { category: index } });

exists(query, options?)

Returns true if at least one object matches the query, false otherwise.

users.exists({ age: { $lt: 18 } });
// => false

// With index hint
users.exists({ category: 'electronics' }, { indexes: { category: index } });

distinct(field, query?, options?)

Returns an array of unique values for the specified field. Optionally filter items first with a query, and pass index hints via options.

users.distinct('role');
// => ['user', 'admin']

// With query filter - only distinct categories for active products
products.distinct('category', { active: true });
// => ['electronics', 'clothing']

// Also supports dotted paths
users.distinct('address.city');
// => ['New York', 'Los Angeles']

// $field syntax (leading $)
users.distinct('$role');
// => ['user', 'admin']

// With index hint for performance
products.distinct('price', { category: 'electronics' }, { indexes: { category: categoryIndex } });

aggregate(pipeline)

Execute an aggregation pipeline. See Aggregation Pipeline section.

users.aggregate([{ $match: { age: { $gte: 18 } } }, { $group: { _id: '$role', count: { $sum: 1 } } }]);

toArray()

Returns a deep copy of the raw array.

users.toArray();
// => [{ ... }, { ... }, { ... }]

Cursor Methods

When you call find(), you get a cursor that supports chaining:

sort(spec)

Sort results by one or more fields.

// Single field, ascending
users.find({}).sort({ name: 1 }).toArray();

// Single field, descending
users.find({}).sort({ age: -1 }).toArray();

// Multiple fields
users.find({}).sort({ role: 1, age: -1 }).toArray();

skip(n)

Skip the first n results (useful for pagination).

users.find({}).skip(10).toArray();

limit(n)

Limit results to n items.

users.find({}).limit(5).toArray();

project(spec)

Apply field projection (alternative to passing projection to find()).

users.find({}).project({ name: 1, email: 1 }).toArray();

toArray()

Execute the query and return results as an array.

users.find({ active: true }).sort({ createdAt: -1 }).limit(10).toArray();

Query Operators

Comparison Operators

Operator Description Example
$eq Equal to { age: { $eq: 30 } } or { age: 30 }
$ne Not equal to { role: { $ne: 'admin' } }
$gt Greater than { price: { $gt: 100 } }
$gte Greater than or equal { age: { $gte: 18 } }
$lt Less than { stock: { $lt: 10 } }
$lte Less than or equal { rating: { $lte: 3 } }
$in In array { status: { $in: ['active', 'pending'] } }
$nin Not in array { role: { $nin: ['guest', 'banned'] } }

Logical Operators

Operator Description Example
$and All conditions must match { $and: [{ age: { $gt: 18 } }, { role: 'user' }] }
$or At least one condition matches { $or: [{ role: 'admin' }, { role: 'moderator' }] }
$not Inverts the query { $not: { age: { $lt: 18 } } }
$nor None of the conditions match { $nor: [{ status: 'deleted' }, { banned: true }] }

Element Operators

Operator Description Example
$exists Field exists (or not) { email: { $exists: true } }
$type Field type check { age: { $type: 'number' } }

Type values: 'string', 'number', 'boolean', 'array', 'object', 'null', 'undefined', 'date'

Array Operators

Operator Description Example
$all Array contains all values { tags: { $all: ['javascript', 'nodejs'] } }
$size Array has exact length { tags: { $size: 3 } }
$elemMatch Array contains matching element { items: { $elemMatch: { price: { $gt: 100 } } } }

Evaluation Operators

Operator Description Example
$regex Regular expression match { name: { $regex: '^A', $options: 'i' } }
$mod Modulo operation { qty: { $mod: [4, 0] } } (divisible by 4)
$expr Compare fields with expressions { $expr: { $gt: ['$price', '$cost'] } }

Expression Operators (used in $expr)

Comparison

$eq, $ne, $gt, $gte, $lt, $lte

// Find products where price is less than MSRP
products.find({ $expr: { $lt: ['$price', '$msrp'] } });

Arithmetic

$add, $subtract, $multiply, $divide

// Total value greater than 1000
products.find({
  $expr: { $gt: [{ $multiply: ['$price', '$quantity'] }, 1000] },
});

String

$concat, $toLower, $toUpper, $substr, $strLenCP, $indexOfCP, $split

// Concatenate fields
users.find({
  $expr: { $eq: [{ $concat: ['$firstName', ' ', '$lastName'] }, 'John Doe'] },
});

Array

$size, $arrayElemAt, $filter, $map, $reduce

// Products with more than 5 tags
products.find({ $expr: { $gt: [{ $size: '$tags' }, 5] } });

Date

$year, $month, $dayOfMonth, $hour, $minute, $second

// Posts from 2024
posts.find({ $expr: { $eq: [{ $year: '$publishDate' }, 2024] } });

Conditional

$cond, $ifNull, $switch

// Using $cond (if-then-else)
products.find({
  $expr: {
    $cond: [{ $gt: ['$stock', 0] }, { $eq: ['$status', 'available'] }, { $eq: ['$status', 'sold-out'] }],
  },
});

Type Conversion

$toString, $toInt, $toDouble, $toDate

Nested Properties

Use dot notation to query nested objects:

const data = [
  { name: 'Alice', address: { city: 'New York', zip: '10001' } },
  { name: 'Bob', address: { city: 'Los Angeles', zip: '90001' } },
];

const collection = new Qar(data);

collection.find({ 'address.city': 'New York' }).toArray();
// => [{ name: 'Alice', ... }]

Field Projection

Control which fields are returned in results.

Include/Exclude Fields

// Include only specific fields
users.find({}, { name: 1, email: 1 });
// => [{ _id: 1, name: 'Alice', email: 'alice@...' }, ...]

// Exclude _id
users.find({}, { name: 1, email: 1, _id: 0 });
// => [{ name: 'Alice', email: 'alice@...' }, ...]

// Exclude specific fields
users.find({}, { password: 0, secretKey: 0 });
// => All fields except password and secretKey

Array Slicing with $slice

Limit array elements returned:

// Get only first 3 comments
posts.find({}, { title: 1, comments: { $slice: 3 } });

// Get last 5 items
posts.find({}, { title: 1, recentViews: { $slice: -5 } });

// Multiple slices
posts.find(
  {},
  {
    title: 1,
    comments: { $slice: 3 },
    tags: { $slice: 5 },
  },
);

Positional Operator ($)

Return only the first matching array element:

// Only return the first comment by Alice
posts.find({ 'comments.author': 'Alice' }, { 'comments.$': 1 });

$elemMatch Projection

Return only the first array element matching a condition:

// Only return comments with more than 10 likes
posts.find(
  {},
  {
    title: 1,
    comments: { $elemMatch: { likes: { $gt: 10 } } },
  },
);

Index Optimization (Large Datasets)

For large arrays (10,000+ items), you can dramatically speed up $eq and direct equality queries by providing a pre-built Map index.

const items = [
  { id: 1, category: 'electronics', price: 299 },
  { id: 2, category: 'books', price: 19 },
  { id: 3, category: 'electronics', price: 499 },
  // ... 100,000 more items
];

const q = new Qar(items);

// Build index once (e.g., at app startup)
const categoryIndex = new Map();
for (const item of items) {
  const key = item.category;
  if (!categoryIndex.has(key)) categoryIndex.set(key, []);
  categoryIndex.get(key).push(item);
}

// Use index for fast $eq lookups - 10-100x faster on large datasets
const electronics = q
  .find(
    { category: 'electronics' }, // or { category: { $eq: 'electronics' } }
    {},
    { indexes: { category: categoryIndex } },
  )
  .toArray();

// Index also works with findOne
const firstElectronic = q.findOne({ category: 'electronics' }, {}, { indexes: { category: categoryIndex } });

// Index is also used by count(), exists(), and distinct()
q.count({ category: 'electronics' }, { indexes: { category: categoryIndex } });
q.exists({ category: 'electronics' }, { indexes: { category: categoryIndex } });
q.distinct('price', { category: 'electronics' }, { indexes: { category: categoryIndex } });

Notes:

  • Index only works for top-level $eq or direct value equality (e.g., { field: 'value' })
  • Build the index once and reuse it across queries
  • The index value must be a Map where keys are field values and values are arrays of matching documents
  • Index hints are supported by find(), findOne(), count(), exists(), and distinct()
  • For distinct() with a query filter, the query must use the indexed field with $eq (or direct equality) for the index to be used

Aggregation Pipeline

Perform advanced data processing with aggregation pipelines.

Stages

$match

Filter documents (same syntax as find()):

users.aggregate([{ $match: { age: { $gte: 18 } } }]);

$group

Group documents and calculate aggregated values:

// Count users by role
users.aggregate([
  {
    $group: {
      _id: '$role',
      count: { $sum: 1 },
    },
  },
]);

// Multiple aggregations
products.aggregate([
  {
    $group: {
      _id: '$category',
      totalProducts: { $sum: 1 },
      avgPrice: { $avg: '$price' },
      maxPrice: { $max: '$price' },
      minPrice: { $min: '$price' },
    },
  },
]);

Accumulator Operators:

  • $sum - Sum values
  • $avg - Calculate average
  • $max - Maximum value
  • $min - Minimum value
  • $push - Build array of values
  • $first - First value in group
  • $last - Last value in group

$sort

Sort documents:

users.aggregate([{ $group: { _id: '$city', count: { $sum: 1 } } }, { $sort: { count: -1 } }]);

$project

Reshape documents and create computed fields:

users.aggregate([
  {
    $project: {
      name: 1,
      email: 1,
      fullName: { $concat: ['$firstName', ' ', '$lastName'] },
    },
  },
]);

$limit

Limit number of documents:

users.aggregate([{ $group: { _id: '$role', count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 5 }]);

$unwind

Deconstruct an array field into separate documents:

const posts = new Qar([
  { title: 'Post 1', tags: ['js', 'node'] },
  { title: 'Post 2', tags: ['react', 'js'] },
]);

posts.aggregate([{ $unwind: '$tags' }, { $group: { _id: '$tags', count: { $sum: 1 } } }]);
// => [{ _id: 'js', count: 2 }, { _id: 'node', count: 1 }, { _id: 'react', count: 1 }]

You may also pass an options object instead of a string. The runtime accepts path (or the aliases $path or field) and an optional preserveNullAndEmptyArrays boolean. When preserveNullAndEmptyArrays is true, documents without the array field or with empty arrays are preserved with the unwound field set to null.

// Using object form with aliases and preserveNullAndEmptyArrays
posts.aggregate([
  { $unwind: { path: '$tags', preserveNullAndEmptyArrays: true } },
  { $group: { _id: '$tags', count: { $sum: 1 } } },
]);

Complete Example

// Analyze product sales by category
const salesReport = products.aggregate([
  // Filter in-stock products
  { $match: { inStock: true } },

  // Calculate total value per product
  {
    $project: {
      category: 1,
      totalValue: { $multiply: ['$price', '$quantity'] },
    },
  },

  // Group by category
  {
    $group: {
      _id: '$category',
      totalRevenue: { $sum: '$totalValue' },
      productCount: { $sum: 1 },
      avgValue: { $avg: '$totalValue' },
    },
  },

  // Sort by revenue
  { $sort: { totalRevenue: -1 } },

  // Skip first 2 categories
  { $skip: 2 },

  // Top 10 categories
  { $limit: 10 },
]);

Pipeline Stages

Stage Description
$match Filter documents
$group Group and aggregate
$sort Sort documents
$project Reshape documents
$limit Limit results
$skip Skip N documents
$unwind Deconstruct array
$lookup Join with another collection

$skip Stage

Skips the specified number of documents.

products.aggregate([
  { $match: { inStock: true } },
  { $sort: { price: -1 } },
  { $skip: 5 }, // Skip top 5 most expensive
  { $limit: 10 }, // Get next 10
]);

$lookup Stage (Join)

Performs a left outer join to another collection.

const orders = new Qar(ordersData);
const users = new Qar(usersData);

// Join orders with users
const ordersWithUsers = orders.aggregate([
  {
    $lookup: {
      from: users, // Target collection (Qar instance or array)
      localField: 'userId', // Field in orders
      foreignField: '_id', // Field in users
      as: 'user', // Output array field
    },
  },
  { $unwind: '$user' }, // Flatten the joined array
  { $project: { orderId: 1, 'user.name': 1, 'user.email': 1, total: 1 } },
]);

Note: The from parameter can be either a Qar instance or a plain array of objects.

Examples

JS App Static Data

Perfect for querying JSON data in JavaScript applications:

// app/products/page.tsx
import productsData from '@/data/products.json';
import Qar from 'qarjs';

const products = new Qar(productsData);

export default function ProductsPage({ searchParams }) {
  const filtered = products
    .find({
      category: searchParams.category,
      price: { $lte: Number(searchParams.maxPrice) || 1000 },
      inStock: true,
    })
    .sort({ price: 1 })
    .skip((Number(searchParams.page) - 1) * 20)
    .limit(20)
    .toArray();

  return <ProductList items={filtered} />;
}

API Routes

// app/api/search/route.ts
import data from '@/data/items.json';
import Qar from 'qarjs';

const items = new Qar(data);

export async function GET(request) {
  const q = request.nextUrl.searchParams.get('q');

  const results = items
    .find({
      $or: [{ name: { $regex: q, $options: 'i' } }, { description: { $regex: q, $options: 'i' } }],
    })
    .limit(50)
    .toArray();

  return Response.json(results);
}

Static Site Generation

import blogPosts from './posts.json';
import Qar from 'qarjs';

const posts = new Qar(blogPosts);

// Get published posts by category
const techPosts = posts
  .find({
    category: 'technology',
    published: true,
    publishDate: { $lte: new Date().toISOString() },
  })
  .sort({ publishDate: -1 })
  .toArray();

// Get featured post
const featured = posts.findOne({ featured: true });

// Count drafts
const draftCount = posts.count({ published: false });

// Get all categories with post counts
const categories = posts.aggregate([
  { $match: { published: true } },
  { $group: { _id: '$category', count: { $sum: 1 } } },
  { $sort: { count: -1 } },
]);

Why Qar over alternatives?

Feature Qar mingo sift.js lodash
MongoDB-style queries
Chainable API ⚠️ Limited
Aggregation pipeline
Field projection
Expression operators ✅ 30+
Zero dependencies
Bundle size (gzipped) ✅ ~5KB ❌ ~15KB ✅ ~2KB ❌ ~24KB

Qar is perfect when you:

  • Work with static JSON data in JavaScript applications or static sites
  • Want MongoDB-style queries without a database
  • Need aggregation and data transformation
  • Want a clean, intuitive API
  • Care about bundle size

Not recommended for:

  • Very large datasets - use a real database
  • Real-time data updates - use a database with subscriptions
  • Complex JOIN operations across multiple collections

Advanced Examples

Complex Queries

const users = new Qar(userData);

// Find active admins or moderators over 25
users
  .find({
    $and: [{ age: { $gt: 25 } }, { status: 'active' }, { $or: [{ role: 'admin' }, { role: 'moderator' }] }],
  })
  .toArray();

// Users NOT in specific roles
users
  .find({
    $nor: [{ role: 'guest' }, { role: 'banned' }],
  })
  .toArray();

// Regex search (case-insensitive)
users
  .find({
    email: { $regex: '@gmail\\.com$', $options: 'i' },
  })
  .toArray();

// Compare fields
products
  .find({
    $expr: { $lt: ['$currentPrice', '$originalPrice'] },
  })
  .toArray();

Pagination

const page = 2;
const pageSize = 20;

const results = products
  .find({ category: 'electronics' })
  .sort({ price: 1 })
  .skip((page - 1) * pageSize)
  .limit(pageSize)
  .toArray();

const total = products.count({ category: 'electronics' });
const totalPages = Math.ceil(total / pageSize);

Multiple Collections

const products = new Qar(productsData);
const orders = new Qar(ordersData);
const users = new Qar(usersData);

// Query each independently
const electronics = products.find({ category: 'electronics' }).toArray();
const recentOrders = orders.find({ date: { $gte: '2026-01-01' } }).toArray();
const admins = users.find({ role: 'admin' }).toArray();

TypeScript

Full TypeScript support included:

import Qar from 'qarjs';

interface User {
  id: number;
  name: string;
  age: number;
  role: 'user' | 'admin';
}

const users = new Qar<User>(userData);

// Type-safe queries
const result = users.find({ age: { $gt: 25 } }).toArray(); // User[]
const admin = users.findOne({ role: 'admin' }); // User | null
const roles = users.distinct('role'); // string[]

Browser Support

Works in all modern browsers and Node.js environments:

  • Chrome/Edge: ✅
  • Firefox: ✅
  • Safari: ✅
  • Node.js: ✅ (v14+)

Guidelines

See Code of Conduct, Contributing, and Security Policy.

License

MIT License © 2026 Zsolt Tövis


If you find this project useful, please consider sponsoring me by: Donate via GitHub / Donate via PayPal / Give the repo a Star / Follow me on GitHub / Share the project on X

Made with ❤️ for developers who love clean APIs and don't need a database for everything

About

MongoDB-style queries for plain JavaScript arrays. Simple, lightweight, and perfect for JS Apps, static sites, and JSON data.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages