npm package: @prostojs/uql
Description: Universal Query Language designed for URLs
Author: Artem Maltsev
Latest version: 0.0.1

# @prostojs/uql

**Universal Query Language (UQL) parser** designed for URLs.  
Parses user-friendly query strings (like `field=5&field2='text'|exists(extraField)`) into an **AST** or a **MongoDB query**.  
Automatically extracts `$controls` (e.g. `$page=2`, `$limit=100`, etc.) from the query string.

## Features

- **AST-based**: Parse into an abstract syntax tree representing logical AND/OR conditions, comparison operators, and `exists(...)`.
- **Easy integration**: Convert the AST to your own format or directly to a MongoDB query.
- **Configurable**:
  - Optional `checkField` callback to validate/authorize field names.
  - Optional `castValue` callback to cast raw values to specific types (e.g. `boolean`, `Date`, etc.).
- **Quoted strings**: Supports explicit quoting via single `'...'` or double `"..."`.
- **Handles** `=`, `!=`, `>`, `>=`, `<`, `<=`, `exists(...)`, `!exists(...)`, grouping with parentheses, logical AND (`&`) and OR (`|`).

## Installation

```bash
# with npm
npm install @prostojs/uql

# or with pnpm
pnpm add @prostojs/uql
```

## Usage

### 1. Basic Parsing

```ts
import { UqlParser } from "@prostojs/uql";

const parser = new UqlParser();

// e.g. "field=5&field2='hello'|exists(extraField)"
const result = parser.parse("(field>5|field2='hello')&exists(extraField)&$page=2");

console.log(result.controls); 
// { page: 2 }

console.log(JSON.stringify(result.ast, null, 2));
// Example AST:
// {
//   "type": "and",
//   "items": [
//     {
//       "type": "or",
//       "items": [
//         {
//           "type": "condition",
//           "field": "field",
//           "operator": "gt",
//           "value": 5
//         },
//         {
//           "type": "condition",
//           "field": "field2",
//           "operator": "eq",
//           "value": "hello"
//         }
//       ]
//     },
//     {
//       "type": "condition",
//       "field": "extraField",
//       "operator": "exists"
//     }
//   ]
// }
```

The parser returns an object with:

- **`ast`**: The parsed **abstract syntax tree**.
- **`controls`**: A record of the “control” parameters (any top-level key that starts with `$`).

### 2. Casting Field Values

By default, the parser:
- Parses numeric-looking values (e.g. `123`) as numbers.
- Leaves everything else as strings.
- An empty value (`field=`) is interpreted as `""` (empty string).

If you want your own casting logic (like converting `"true"` → `true`), pass a `castValue` function:

```ts
import { UqlParser } from "@prostojs/uql";

const parserWithCast = new UqlParser({
  castValue(fieldName, operator, rawValue) {
    if (rawValue === "true") return true;
    if (rawValue === "false") return false;
    // fallback
    return rawValue;
  },
});

const result = parserWithCast.parse("flag=true&flag2=false");

console.log(JSON.stringify(result.ast, null, 2));
// {
//   "type": "and",
//   "items": [
//     {
//       "type": "condition",
//       "field": "flag",
//       "operator": "eq",
//       "value": true
//     },
//     {
//       "type": "condition",
//       "field": "flag2",
//       "operator": "eq",
//       "value": false
//     }
//   ]
// }
```

### 3. Validating Fields

You can **validate** or **authorize** field names by providing a `checkField` callback:

```ts
const parserWithFieldCheck = new UqlParser({
  checkField(fieldName) {
    if (!["allowedField", "someOther"].includes(fieldName)) {
      throw new Error(`Field ${fieldName} is not allowed!`);
    }
    return true;
  },
});

try {
  parserWithFieldCheck.parse("secret=123");
} catch (error) {
  console.error(error.message);
  // Field secret is not allowed!
}
```

### 4. Convert to MongoDB Query

This package also ships with a helper `uqlToMongoQuery()` (in `@prostojs/uql/mongo`) to convert the resulting AST into a Mongo query object:

```ts
import { UqlParser } from "@prostojs/uql";
import { uqlToMongoQuery } from "@prostojs/uql/mongo";

const parser = new UqlParser();
const { ast, controls } = parser.parse("(field>5|field2='hello')&exists(extraField)");

if (ast) {
  const mongoQuery = uqlToMongoQuery(ast);
  console.log(JSON.stringify(mongoQuery, null, 2));
  // {
  //   "$and": [
  //     {
  //       "$or": [
  //         { "field": { "$gt": 5 } },
  //         { "field": "hello" }
  //       ]
  //     },
  //     {
  //       "extraField": { "$exists": true }
  //     }
  //   ]
  // }
}
```

### 5. Examples

- `field=123` → AST: `{"type":"condition","field":"field","operator":"eq","value":123}`
- `field='123'` → `value: "123"` (explicit string)
- `!exists(field)` → `{"type":"condition","field":"field","operator":"exists","negateExists":true}`
- `field>10&other!='foo'`
  - → `$and` of `{"field":{"$gt":10}}` and `{"other":{"$ne":"foo"}}` (if you convert to Mongo).

---

## License

[MIT](./LICENSE) © 2025 Artem Maltsev and Contributors.