Skip to content

Commit 3d50f72

Browse files
committed
feat(search-query-language)
1 parent 5a95add commit 3d50f72

23 files changed

+1860
-0
lines changed

docs/index.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,47 @@ hero:
77

88
features:
99
- title: Core
10+
icon: 🚀
1011
details: General utility functions
1112
link: /reference/@andrew_l/toolkit/
1213
- title: DOM
14+
icon: 🏞️
1315
details: Utility functions to simplify animations, clipboard operations, and smooth scrolling.
1416
link: /reference/@andrew_l/dom/
1517
- title: Context
18+
icon: ⚙️
1619
details: Like composition api but for Node.
1720
link: /reference/@andrew_l/context/
1821
- title: Graceful
22+
icon: 💤
1923
details: Utility to manage application shutdown.
2024
link: /reference/@andrew_l/graceful/
2125
- title: IOC
26+
icon: 🚢
2227
details: Pretty simple IOC Container.
2328
link: /reference/@andrew_l/ioc/
2429
- title: Service Actor
30+
icon: 🪪
2531
details: Forget about passing data like trace IDs between functions.
2632
link: /reference/@andrew_l/service-actor/
2733
- title: Mongo Transaction
34+
icon: 🪗
2835
details: Manages side effects in MongoDB transactions, rollback on failure and preventing duplicates on retries.
2936
link: /reference/@andrew_l/mongo-transaction/
3037
- title: Mongo Pagination
38+
icon: 📜
3139
details: Manages pagination without relying on traditional offsets.
3240
link: /reference/@andrew_l/mongo-pagination/
41+
- title: Search Query Language
42+
icon: 🔍
43+
details: Converts human-readable query strings into structured representations.
44+
link: /reference/@andrew_l/search-query-language/
3345
- title: TL Pack
46+
icon: 📦
3447
details: Another implementation of binary serialization.
3548
link: /reference/@andrew_l/tl-pack/
3649
- title: Vue Stdout
50+
icon: 🪄
3751
details: Renderer for terminal output with flexible layouts and CLI components.
3852
link: /reference/@andrew_l/vue-stdout/
3953
---
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Search Query Language <!-- omit in toc -->
2+
3+
![license](https://img.shields.io/npm/l/query-parser) <!-- omit in toc -->
4+
![npm version](https://img.shields.io/npm/v/query-parser) <!-- omit in toc -->
5+
![npm bundle size](https://img.shields.io/bundlephobia/minzip/query-parser) <!-- omit in toc -->
6+
7+
Search Query Language is a lightweight utility that converts human-readable query strings into structured representations. It supports parsing expressions into an abstract syntax tree (AST) and transforming them into queries.
8+
9+
[Documentation](https://your-docs-link.com)
10+
11+
<!-- install placeholder -->
12+
13+
## ✨ Features
14+
15+
- **Expression Parsing**: Convert query strings into an AST for structured processing.
16+
- **MongoDB Query Conversion**: Transform expressions into MongoDB-compatible query objects.
17+
- **Flexible Syntax Support**: Supports comparison operators like `=`, `!=`, `>`, `<`, `>=`, `<=`.
18+
19+
## 🔍 Search Syntax
20+
21+
The following table lists the syntax that you can use to construct a query.
22+
23+
| Syntax | Usage | Description | Examples |
24+
| ------ | ------------------------------------------- | -------------------------------------------- | ----------------------------------------------- |
25+
| `=` | `field="value"` | Exact match | `name="andrew"` |
26+
| `!=` | `field!="value"` | Not equal to | `status!="active"` |
27+
| `<` | `field<value` | Less than | `amount<5000` |
28+
| `<=` | `field<=value` | Less than or equal to | `amount<=10000` |
29+
| `>` | `field>value` | Greater than | `created>1672531200` |
30+
| `>=` | `field>=value` | Greater than or equal to | `created>=1672531200` |
31+
| `AND` | `condition1 AND condition2` | Combine conditions (both must be true) | `status="active" AND age>=18` |
32+
| `OR` | `condition1 OR condition2` | Combine conditions (either can be true) | `status="stop" OR age>40` |
33+
| `()` | `(condition1 OR condition2) AND condition3` | Group conditions to control logic precedence | `(status="active" OR status="stop") AND age>18` |
34+
35+
## 📌 Notes
36+
37+
- The **left operand** must always be an identifier (e.g., a field name), while the **right operand** must always be a literal value (e.g., a string, number, or boolean).
38+
- Support **comparison operators (`<`, `<=`, `>`, `>=`)**.
39+
- Use **`AND`** and **`OR`** to combine conditions.
40+
- Parentheses **`()`** help group conditions for better control over query logic.
41+
42+
## 🚀 Example: Minimal
43+
44+
```ts
45+
import { parseToMongo } from '@andrew_l/search-query-language';
46+
47+
const clients = db.collection('clients');
48+
49+
// GET /clients?search="age>18"
50+
app.get('/clients', async (req, res) => {
51+
const filter = parseToMongo(req.query.search);
52+
const items = await clients.find(filter).toArray();
53+
res.json(items);
54+
});
55+
```
56+
57+
## 🚀 Example: Usage MongoDB
58+
59+
```ts
60+
import { parseToMongo } from '@andrew_l/search-query-language';
61+
62+
// GET /clients?search="active=true AND age>18"
63+
app.get('/clients', async (req, res) => {
64+
const filter = parseToMongo(req.query.search, {
65+
transform: {
66+
_id: [MONGO_TRANSFORM.OBJECT_ID, MONGO_TRANSFORM.NOT_NULLABLE],
67+
},
68+
});
69+
70+
const items = await db.collection('clients').find(filter).toArray();
71+
72+
res.json(items);
73+
});
74+
```
75+
76+
## 🚀 Example: Usage Mongoose
77+
78+
```ts
79+
import mongoose from 'mongoose';
80+
import { parseToMongoose } from '@andrew_l/search-query-language';
81+
82+
const Client = mongoose.Model('Clients', new mongoose.Schema({
83+
email: String;
84+
active: Boolean;
85+
}))
86+
87+
// GET /clients?search="email="andrew.io.dev@gmail.com" AND active=true"
88+
app.get('/clients', async (req, res) => {
89+
const filter = parseToMongoose(Client, req.query.search);
90+
const items = await Client.find(filter).lean();
91+
92+
res.json(items);
93+
});
94+
```
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineBuildConfig } from 'unbuild';
2+
3+
export default defineBuildConfig({
4+
entries: ['src/index.ts'],
5+
declaration: true,
6+
sourcemap: true,
7+
rollup: {
8+
emitCJS: true,
9+
},
10+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "@andrew_l/search-query-language",
3+
"version": "0.2.13",
4+
"license": "MIT",
5+
"type": "module",
6+
"keywords": [
7+
"search",
8+
"query",
9+
"query language",
10+
"mongo",
11+
"mongodb",
12+
"mongoose"
13+
],
14+
"repository": {
15+
"type": "git",
16+
"url": "git+https://github.com/men232/toolkit.git",
17+
"directory": "packages/search-query-language"
18+
},
19+
"scripts": {
20+
"build": "unbuild",
21+
"prepack": "unbuild",
22+
"test": "vitest run --typecheck",
23+
"test:watch": "vitest watch --typecheck"
24+
},
25+
"exports": {
26+
".": {
27+
"import": "./dist/index.mjs",
28+
"require": "./dist/index.cjs"
29+
}
30+
},
31+
"main": "./dist/index.cjs",
32+
"types": "./dist/index.d.ts",
33+
"files": [
34+
"dist"
35+
],
36+
"devDependencies": {
37+
"@types/node": "catalog:",
38+
"typescript": "catalog:",
39+
"unbuild": "catalog:",
40+
"vitest": "catalog:"
41+
},
42+
"peerDependencies": {
43+
"mongoose": "^7 || ^8"
44+
},
45+
"dependencies": {
46+
"@andrew_l/toolkit": "workspace:*"
47+
}
48+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { NODE } from './constants';
3+
import { Expression } from './Expression';
4+
import type { NodeExpression } from './types';
5+
6+
function t(input: string): NodeExpression[] {
7+
return new Expression(input).parse().body;
8+
}
9+
10+
describe('Expression', () => {
11+
it('should handle string equality expression', () => {
12+
expect(t('name = "andrew"')).toStrictEqual([
13+
{
14+
type: NODE.BINARY_EXPRESSION,
15+
start: 0,
16+
end: 15,
17+
left: {
18+
start: 0,
19+
end: 5,
20+
type: NODE.IDENTIFIER,
21+
name: 'name',
22+
},
23+
operator: '=',
24+
right: {
25+
start: 7,
26+
end: 15,
27+
type: NODE.LITERAL,
28+
value: 'andrew',
29+
raw: '"andrew"',
30+
},
31+
},
32+
]);
33+
34+
expect(t('name != "andrew"')).toStrictEqual([
35+
{
36+
type: NODE.BINARY_EXPRESSION,
37+
start: 0,
38+
end: 16,
39+
left: {
40+
start: 0,
41+
end: 5,
42+
type: NODE.IDENTIFIER,
43+
name: 'name',
44+
},
45+
operator: '!=',
46+
right: {
47+
start: 8,
48+
end: 16,
49+
type: NODE.LITERAL,
50+
value: 'andrew',
51+
raw: '"andrew"',
52+
},
53+
},
54+
]);
55+
});
56+
57+
it('should handle number equality expression', () => {
58+
expect(t('age > 5')).toStrictEqual([
59+
{
60+
type: NODE.BINARY_EXPRESSION,
61+
start: 0,
62+
end: 7,
63+
left: {
64+
start: 0,
65+
end: 4,
66+
type: NODE.IDENTIFIER,
67+
name: 'age',
68+
},
69+
operator: '>',
70+
right: {
71+
start: 6,
72+
end: 7,
73+
type: NODE.LITERAL,
74+
value: 5,
75+
raw: '5',
76+
},
77+
},
78+
]);
79+
});
80+
81+
it('should handle number with minus equality expression', () => {
82+
expect(t('age > -5')).toStrictEqual([
83+
{
84+
type: NODE.BINARY_EXPRESSION,
85+
start: 0,
86+
end: 8,
87+
left: {
88+
start: 0,
89+
end: 4,
90+
type: NODE.IDENTIFIER,
91+
name: 'age',
92+
},
93+
operator: '>',
94+
right: {
95+
start: 6,
96+
end: 8,
97+
type: NODE.LITERAL,
98+
value: -5,
99+
raw: '-5',
100+
},
101+
},
102+
]);
103+
});
104+
105+
describe('binary expression', () => {
106+
it('should throw error when left side not a identifier', () => {
107+
expect(() => t('5 > age')).toThrowError(
108+
'Expected identifier as left side of binary expression.',
109+
);
110+
});
111+
112+
it('should throw error when right side not a literal', () => {
113+
expect(() => t('ego > balance')).toThrowError(
114+
'Expected literal as right side of binary expression.',
115+
);
116+
});
117+
});
118+
119+
describe('logical expression', () => {
120+
it('should throw error when left side not an expression', () => {
121+
expect(() => t('age OR age > 5')).toThrowError(
122+
'Expected expression as left side of logical expression.',
123+
);
124+
});
125+
126+
it('should throw error when right side not an expression', () => {
127+
expect(() => t('age > 5 OR age')).toThrowError(
128+
'Expected operator after identifier',
129+
);
130+
});
131+
});
132+
});

0 commit comments

Comments
 (0)