1.5.0 • Published 10 months ago

@abaktiar/ql-parser v1.5.0

Weekly downloads
-
License
MIT
Repository
github
Last release
10 months ago

@abaktiar/ql-parser

A powerful, framework-agnostic QL (Query Language) parser and builder for creating complex queries with support for logical operators, functions, parentheses grouping, and ORDER BY clauses.

🌐 Live Demo - See the parser in action with the React component!

npm version TypeScript License: MIT

šŸŽ‰ What's New in v1.5.0

šŸ”§ Enhanced Function System - Comprehensive parameterized function support with type validation
🧮 Improved Expression Trees - Better parsing for complex nested expressions and function calls
šŸŽÆ Parameter Validation - Robust validation for function parameters with descriptive error messages
šŸ“ Extended Documentation - Comprehensive examples and migration guides for parameterized functions

✨ Features

  • šŸ” Complete Query Parsing - Parse complex QL queries into structured AST
  • 🧮 Expression Trees - Build hierarchical expression trees with proper operator precedence
  • šŸ”§ Query Building - Convert parsed queries to MongoDB, SQL, or custom formats
  • šŸ“ TypeScript Support - Full type safety with comprehensive interfaces
  • šŸŽÆ Parentheses Grouping - Support for complex condition grouping
  • šŸ“Š ORDER BY Support - Sorting clauses with ASC/DESC directions
  • šŸ”„ Function Support - Built-in functions like currentUser(), now(), etc.
  • ⚔ Zero Dependencies - Lightweight with no external dependencies
  • 🌐 Framework Agnostic - Works with any JavaScript framework or vanilla JS

šŸ“¦ Installation

npm install @abaktiar/ql-parser
yarn add @abaktiar/ql-parser
pnpm add @abaktiar/ql-parser

šŸš€ Quick Start

import { QLParser, QLInputConfig } from '@abaktiar/ql-parser';

// Define your field configuration
const config: QLInputConfig = {
  fields: [
    {
      name: 'status',
      displayName: 'Status',
      type: 'option',
      operators: ['=', '!=', 'IN', 'NOT IN'],
      options: [
        { value: 'open', label: 'Open' },
        { value: 'closed', label: 'Closed' },
        { value: 'pending', label: 'Pending' }
      ]
    },
    {
      name: 'assignee',
      displayName: 'Assignee',
      type: 'user',
      operators: ['=', '!=', 'IS EMPTY', 'IS NOT EMPTY']
    },
    {
      name: 'created',
      displayName: 'Created Date',
      type: 'date',
      operators: ['=', '>', '<', '>=', '<='],
      sortable: true
    }
  ],
  maxSuggestions: 10,
  caseSensitive: false,
  allowParentheses: true,
  allowOrderBy: true,
  allowFunctions: true
};

// Create parser instance
const parser = new QLParser(config);

// Parse a query
const query = parser.parse('status = "open" AND (assignee = currentUser() OR assignee IS EMPTY) ORDER BY created DESC');

console.log(query);
// Output: Structured QLQuery object with parsed expressions and order by clause

šŸ“– Core Concepts

Query Structure

A QL query consists of: 1. WHERE clause - Conditions and logical operators 2. ORDER BY clause - Sorting specifications (optional)

status = "open" AND assignee = currentUser() ORDER BY created DESC, priority ASC

Supported Operators

Comparison Operators

  • = - Equals
  • != - Not equals
  • > - Greater than
  • < - Less than
  • >= - Greater than or equal
  • <= - Less than or equal

Text Operators

  • ~ - Contains (case-insensitive)
  • !~ - Does not contain

List Operators

  • IN - Value in list
  • NOT IN - Value not in list

Null Operators

  • IS EMPTY - Field is empty/null
  • IS NOT EMPTY - Field is not empty/null

Logical Operators

  • AND - Logical AND
  • OR - Logical OR
  • NOT - Logical NOT (prefix)

Functions

Built-in functions for dynamic values:

Parameterless Functions

  • currentUser() - Current user identifier
  • now() - Current timestamp
  • today() - Today's date
  • startOfWeek() - Start of current week
  • endOfWeek() - End of current week
  • startOfMonth() - Start of current month
  • endOfMonth() - End of current month

Parameterized Functions

  • daysAgo(days) - Date N days ago from today
    daysAgo(30)  // 30 days ago
    daysAgo(7)   // 7 days ago
  • daysFromNow(days) - Date N days from today
    daysFromNow(14)  // 14 days from now
  • dateRange(start, end) - Date range between two dates
    dateRange("2023-01-01", "2023-12-31")
  • userInRole(role) - Users with specific role
    userInRole("admin")
    userInRole("manager")
  • projectsWithPrefix(prefix) - Projects with name prefix
    projectsWithPrefix("PROJ")
    projectsWithPrefix("DEV")

Function Usage in Queries

Functions can be used in various contexts:

  • Single conditions: created >= daysAgo(30)
  • IN lists: assignee IN (currentUser(), userInRole("admin"))
  • Complex expressions: (created >= daysAgo(30) AND assignee = currentUser()) OR priority = "High"
  • Nested functions: dateRange("2023-01-01", daysAgo(30))

šŸ”§ API Reference

QLParser

Main parser class for tokenizing and parsing QL queries.

class QLParser {
  constructor(config: QLInputConfig)

  // Tokenize input string into tokens
  tokenize(input: string): QLToken[]

  // Parse input string into structured query
  parse(input: string): QLQuery
}

QLExpressionParser

Builds hierarchical expression trees from tokens.

class QLExpressionParser {
  // Parse tokens into expression tree
  parse(tokens: QLToken[]): QLExpression | null
}

Query Builder Functions

Convert parsed queries to different formats:

// Convert to MongoDB query
function toMongooseQuery(query: QLQuery): object

// Convert to SQL WHERE clause
function toSQLQuery(query: QLQuery): string

// Print expression as readable string
function printExpression(expression: QLExpression): string

// Count total conditions in query
function countConditions(query: QLQuery): number

šŸ’” Usage Examples

Basic Parsing

const parser = new QLParser(config);

// Simple condition
const simple = parser.parse('status = "open"');

// Multiple conditions with AND
const multiple = parser.parse('status = "open" AND priority = "high"');

// Conditions with OR
const withOr = parser.parse('status = "open" OR status = "pending"');

// Complex grouping with parentheses
const complex = parser.parse('(status = "open" OR status = "pending") AND assignee = currentUser()');

Working with Functions

// Using parameterless functions
const withFunctions = parser.parse(`
  assignee = currentUser() AND
  created >= startOfWeek() AND
  created <= endOfWeek()
`);

// Using parameterized functions
const withParams = parser.parse(`
  assignee = userInRole("admin") AND
  created >= daysAgo(30) AND
  updated <= daysFromNow(7)
`);

// Functions with multiple parameters
const multiParam = parser.parse('created = dateRange("2023-01-01", "2023-12-31")');

// Functions in lists
const functionList = parser.parse('assignee IN (currentUser(), userInRole("manager"), "john.doe")');

// Nested function calls
const nested = parser.parse('created = dateRange("2023-01-01", daysAgo(30))');

// Complex expressions with functions
const complex = parser.parse(`
  (assignee = currentUser() OR assignee = userInRole("admin")) AND
  created >= daysAgo(30) AND
  project = projectsWithPrefix("PROJ")
`);

ORDER BY Clauses

// Single sort field
const singleSort = parser.parse('status = "open" ORDER BY created DESC');

// Multiple sort fields
const multiSort = parser.parse('status = "open" ORDER BY priority ASC, created DESC');

// Sort only (no WHERE clause)
const sortOnly = parser.parse('ORDER BY created DESC');

Converting to Different Formats

import { toMongooseQuery, toSQLQuery, printExpression } from '@abaktiar/ql-parser';

const query = parser.parse('status = "open" AND assignee = currentUser()');

// Convert to MongoDB query
const mongoQuery = toMongooseQuery(query);
console.log(mongoQuery);
// Output: { status: "open", assignee: "currentUser()" }

// Convert to SQL
const sqlQuery = toSQLQuery(query);
console.log(sqlQuery);
// Output: "status = 'open' AND assignee = 'currentUser()'"

// Print as readable string
if (query.where) {
  const readable = printExpression(query.where);
  console.log(readable);
  // Output: "status = "open" AND assignee = currentUser()"
}

Function Configuration

You can define custom functions with parameters in your configuration:

const config: QLInputConfig = {
  fields: [...],
  allowFunctions: true,
  functions: [
    {
      name: 'currentUser',
      displayName: 'currentUser()',
      description: 'Current logged in user',
    },
    {
      name: 'daysAgo',
      displayName: 'daysAgo(days)',
      description: 'Date N days ago from today',
      parameters: [{
        name: 'days',
        type: 'number',
        required: true,
        description: 'Number of days'
      }]
    },
    {
      name: 'userInRole',
      displayName: 'userInRole(role)',
      description: 'Users with specific role',
      parameters: [{
        name: 'role',
        type: 'text',
        required: true,
        description: 'User role name'
      }]
    },
    {
      name: 'dateRange',
      displayName: 'dateRange(start, end)',
      description: 'Date range between two dates',
      parameters: [
        {
          name: 'startDate',
          type: 'date',
          required: true,
          description: 'Start date'
        },
        {
          name: 'endDate',
          type: 'date',
          required: true,
          description: 'End date'
        }
      ]
    }
  ]
};

Function Parameter Types

  • text - String parameters (quoted in queries)
  • number - Numeric parameters (unquoted)
  • date - Date parameters (quoted)
  • boolean - Boolean parameters (true/false)

Error Handling

const query = parser.parse('invalid query syntax');

if (!query.valid) {
  console.log('Parse errors:', query.errors);
  // Handle parsing errors
}

Working with Tokens

// Get detailed token information
const tokens = parser.tokenize('status = "open" AND assignee = currentUser()');

tokens.forEach(token => {
  console.log(`${token.type}: "${token.value}" at position ${token.start}-${token.end}`);
});

šŸŽÆ Advanced Usage

Custom Field Types

const config: QLInputConfig = {
  fields: [
    {
      name: 'customField',
      displayName: 'Custom Field',
      type: 'text', // or 'number', 'date', 'datetime', 'boolean', 'option', 'multiselect', 'user'
      operators: ['=', '!=', '~', '!~'],
      description: 'A custom field for demonstration'
    }
  ],
  // ... other config options
};

Complex Expressions

// Nested parentheses
const nested = parser.parse(`
  (status = "open" OR status = "pending") AND
  (assignee = currentUser() OR (assignee IS EMPTY AND priority = "high"))
`);

// Mixed operators
const mixed = parser.parse(`
  status IN ("open", "pending") AND
  title ~ "urgent" AND
  created >= startOfWeek() AND
  assignee IS NOT EMPTY
`);

Expression Tree Traversal

function traverseExpression(expr: QLExpression, depth = 0): void {
  const indent = '  '.repeat(depth);

  if (expr.type === 'condition') {
    console.log(`${indent}Condition: ${expr.field} ${expr.operator} ${expr.value}`);
  } else if (expr.type === 'group') {
    console.log(`${indent}Group (${expr.operator}):`);
    expr.conditions.forEach(child => traverseExpression(child, depth + 1));
  }
}

const query = parser.parse('(status = "open" OR status = "pending") AND assignee = currentUser()');
if (query.where) {
  traverseExpression(query.where);
}

šŸ“š Type Definitions

Core Types

// Field configuration
interface QLField {
  name: string;
  displayName: string;
  type: FieldType;
  operators: OperatorType[];
  sortable?: boolean;
  description?: string;
  options?: QLValue[];
  asyncValueSuggestions?: boolean;
}

// Parsed query result
interface QLQuery {
  where?: QLExpression;
  orderBy?: QLOrderBy[];
  raw: string;
  valid: boolean;
  errors: string[];
}

// Expression tree node
interface QLExpression {
  type: 'condition' | 'group';
  // ... additional properties based on type
}

šŸ¤ Related Packages

šŸ“„ License

MIT Ā© abaktiar

šŸ› Issues & Support

Please report issues on GitHub Issues.

1.5.0

10 months ago

1.4.0

10 months ago

1.3.1

10 months ago

1.3.0

10 months ago

1.2.0

10 months ago

1.1.0

10 months ago

1.0.1

10 months ago

1.0.0

10 months ago