@vbnz/conditions 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,259 @@
1
+ # @vbnz/conditions
2
+
3
+ TypeScript библиотека для проверки условий по данным.
4
+
5
+ ## Установка
6
+
7
+ ```bash
8
+ npm install @vbnz/conditions
9
+ ```
10
+
11
+ ## Базовое использование
12
+
13
+ ```ts
14
+ import { check } from "@vbnz/conditions";
15
+
16
+ const result = check(
17
+ { "{{ profile.uuid }}": { $eq: "some uuid" } },
18
+ { profile: { uuid: "some uuid" } }
19
+ );
20
+
21
+ console.log(result); // true
22
+ ```
23
+
24
+ Библиотека поддерживает шаблонные ключи вида `{{ path.to.value }}` для доступа к вложенным полям.
25
+
26
+ ## Операторы сравнения
27
+
28
+ ### `$eq` — равно
29
+
30
+ ```ts
31
+ check({ status: { $eq: "active" } }, { status: "active" }); // true
32
+ check({ "{{ user.age }}": { $eq: 25 } }, { user: { age: 25 } }); // true
33
+ ```
34
+
35
+ ### `$ne` — не равно
36
+
37
+ ```ts
38
+ check({ status: { $ne: "blocked" } }, { status: "active" }); // true
39
+ check({ "{{ user.age }}": { $ne: 18 } }, { user: { age: 25 } }); // true
40
+ ```
41
+
42
+ ### `$gt` — больше
43
+
44
+ ```ts
45
+ check({ "{{ user.age }}": { $gt: 18 } }, { user: { age: 25 } }); // true
46
+ check({ "{{ score }}": { $gt: 100 } }, { score: 50 }); // false
47
+ ```
48
+
49
+ ### `$gte` — больше или равно
50
+
51
+ ```ts
52
+ check({ "{{ user.age }}": { $gte: 18 } }, { user: { age: 18 } }); // true
53
+ check({ "{{ user.age }}": { $gte: 21 } }, { user: { age: 18 } }); // false
54
+ ```
55
+
56
+ ### `$lt` — меньше
57
+
58
+ ```ts
59
+ check({ "{{ user.age }}": { $lt: 30 } }, { user: { age: 25 } }); // true
60
+ check({ "{{ score }}": { $lt: 50 } }, { score: 50 }); // false
61
+ ```
62
+
63
+ ### `$lte` — меньше или равно
64
+
65
+ ```ts
66
+ check({ "{{ user.age }}": { $lte: 30 } }, { user: { age: 30 } }); // true
67
+ check({ "{{ user.age }}": { $lte: 20 } }, { user: { age: 25 } }); // false
68
+ ```
69
+
70
+ ### `$between` — в диапазоне (включительно)
71
+
72
+ ```ts
73
+ check({ "{{ score }}": { $between: [0, 100] } }, { score: 75 }); // true
74
+ check({ "{{ score }}": { $between: [0, 50] } }, { score: 75 }); // false
75
+ ```
76
+
77
+ ### `$notBetween` — вне диапазона
78
+
79
+ ```ts
80
+ check({ "{{ score }}": { $notBetween: [0, 50] } }, { score: 75 }); // true
81
+ check({ "{{ score }}": { $notBetween: [0, 100] } }, { score: 75 }); // false
82
+ ```
83
+
84
+ ## Операторы включения
85
+
86
+ ### `$in` — значение в массиве
87
+
88
+ ```ts
89
+ check({ status: { $in: ["active", "pending"] } }, { status: "active" }); // true
90
+ check({ status: { $in: ["blocked"] } }, { status: "active" }); // false
91
+ ```
92
+
93
+ ### `$notIn` — значение не в массиве
94
+
95
+ ```ts
96
+ check({ status: { $notIn: ["blocked", "deleted"] } }, { status: "active" }); // true
97
+ check({ status: { $notIn: ["active"] } }, { status: "active" }); // false
98
+ ```
99
+
100
+ ## Строковые операторы
101
+
102
+ ### `$like` — шаблон SQL LIKE (`%` — любые символы, `_` — один символ)
103
+
104
+ ```ts
105
+ check({ name: { $like: "John%" } }, { name: "John Doe" }); // true
106
+ check({ name: { $like: "%Doe" } }, { name: "John Doe" }); // true
107
+ check({ name: { $like: "J_hn" } }, { name: "John" }); // true
108
+ check({ name: { $like: "john%" } }, { name: "John Doe" }); // false (регистрозависимый)
109
+ ```
110
+
111
+ ### `$iLike` — регистронезависимый LIKE
112
+
113
+ ```ts
114
+ check({ name: { $iLike: "john%" } }, { name: "John Doe" }); // true
115
+ check({ name: { $iLike: "%DOE" } }, { name: "John Doe" }); // true
116
+ ```
117
+
118
+ ### `$startsWith` — начинается с
119
+
120
+ ```ts
121
+ check({ name: { $startsWith: "John" } }, { name: "John Doe" }); // true
122
+ check({ name: { $startsWith: "Doe" } }, { name: "John Doe" }); // false
123
+ ```
124
+
125
+ ### `$endsWith` — заканчивается на
126
+
127
+ ```ts
128
+ check({ name: { $endsWith: "Doe" } }, { name: "John Doe" }); // true
129
+ check({ name: { $endsWith: "John" } }, { name: "John Doe" }); // false
130
+ ```
131
+
132
+ ### `$substring` — содержит подстроку
133
+
134
+ ```ts
135
+ check({ name: { $substring: "hn D" } }, { name: "John Doe" }); // true
136
+ check({ name: { $substring: "xyz" } }, { name: "John Doe" }); // false
137
+ ```
138
+
139
+ ### `$regexp` — соответствует регулярному выражению
140
+
141
+ ```ts
142
+ check({ email: { $regexp: "^.+@.+\\.com$" } }, { email: "test@example.com" }); // true
143
+ check({ email: { $regexp: /^.+@.+\.com$/ } }, { email: "test@example.com" }); // true (RegExp объект)
144
+ check({ email: { $regexp: "\\.org$" } }, { email: "test@example.com" }); // false
145
+ ```
146
+
147
+ ### `$notRegexp` — не соответствует регулярному выражению
148
+
149
+ ```ts
150
+ check({ email: { $notRegexp: "\\.org$" } }, { email: "test@example.com" }); // true
151
+ check({ email: { $notRegexp: "\\.com$" } }, { email: "test@example.com" }); // false
152
+ ```
153
+
154
+ ## Операторы для массивов и объектов
155
+
156
+ ### `$contains` — содержит элемент / подстроку / подобъект
157
+
158
+ Для строк:
159
+
160
+ ```ts
161
+ check({ bio: { $contains: "developer" } }, { bio: "I am a developer" }); // true
162
+ ```
163
+
164
+ Для массивов:
165
+
166
+ ```ts
167
+ check({ tags: { $contains: "typescript" } }, { tags: ["js", "ts", "typescript"] }); // true
168
+ check({ tags: { $contains: ["js", "ts"] } }, { tags: ["js", "ts", "node"] }); // true (все элементы)
169
+ ```
170
+
171
+ Для объектов:
172
+
173
+ ```ts
174
+ check(
175
+ { profile: { $contains: { role: "admin" } } },
176
+ { profile: { role: "admin", name: "John" } }
177
+ ); // true
178
+ ```
179
+
180
+ ### `$overlap` — пересечение массивов
181
+
182
+ ```ts
183
+ check({ tags: { $overlap: ["ts", "python"] } }, { tags: ["js", "ts", "node"] }); // true (есть "ts")
184
+ check({ tags: { $overlap: ["go", "rust"] } }, { tags: ["js", "ts"] }); // false
185
+ ```
186
+
187
+ ## Специальные операторы
188
+
189
+ ### `$is` — точное совпадение (аналог `$eq`, для явности)
190
+
191
+ ```ts
192
+ check({ value: { $is: null } }, { value: null }); // true
193
+ check({ value: { $is: undefined } }, { value: undefined }); // true
194
+ ```
195
+
196
+ ### `$not` — отрицание условия
197
+
198
+ ```ts
199
+ check({ status: { $not: { $eq: "blocked" } } }, { status: "active" }); // true
200
+ check({ "{{ user.age }}": { $not: { $lt: 18 } } }, { user: { age: 25 } }); // true
201
+ ```
202
+
203
+ ## Логические операторы
204
+
205
+ ### `$and` — логическое И
206
+
207
+ ```ts
208
+ check(
209
+ {
210
+ $and: [
211
+ { "{{ user.age }}": { $gte: 18 } },
212
+ { "{{ user.verified }}": { $eq: true } }
213
+ ]
214
+ },
215
+ { user: { age: 25, verified: true } }
216
+ ); // true
217
+ ```
218
+
219
+ ### `$or` — логическое ИЛИ
220
+
221
+ ```ts
222
+ check(
223
+ {
224
+ $or: [
225
+ { role: { $eq: "admin" } },
226
+ { role: { $eq: "moderator" } }
227
+ ]
228
+ },
229
+ { role: "moderator" }
230
+ ); // true
231
+ ```
232
+
233
+ ### `$not` на уровне условия — отрицание всего блока
234
+
235
+ ```ts
236
+ check(
237
+ { $not: { status: { $eq: "blocked" } } },
238
+ { status: "active" }
239
+ ); // true
240
+ ```
241
+
242
+ ## Комбинирование операторов
243
+
244
+ Операторы можно комбинировать в одном условии:
245
+
246
+ ```ts
247
+ check(
248
+ {
249
+ "{{ user.age }}": { $gte: 18, $lte: 65 },
250
+ status: { $in: ["active", "pending"] },
251
+ name: { $iLike: "%john%" }
252
+ },
253
+ {
254
+ user: { age: 25 },
255
+ status: "active",
256
+ name: "John Doe"
257
+ }
258
+ ); // true
259
+ ```
@@ -0,0 +1,33 @@
1
+ export type Primitive = string | number | boolean | null | undefined;
2
+ export type OperatorCondition = {
3
+ $eq?: unknown;
4
+ $ne?: unknown;
5
+ $gt?: number;
6
+ $gte?: number;
7
+ $lt?: number;
8
+ $lte?: number;
9
+ $in?: unknown[];
10
+ $notIn?: unknown[];
11
+ $between?: [number, number];
12
+ $notBetween?: [number, number];
13
+ $like?: string;
14
+ $iLike?: string;
15
+ $startsWith?: string;
16
+ $endsWith?: string;
17
+ $substring?: string;
18
+ $regexp?: string | RegExp;
19
+ $notRegexp?: string | RegExp;
20
+ $contains?: unknown;
21
+ $overlap?: unknown[];
22
+ $is?: unknown;
23
+ $not?: FieldCondition;
24
+ };
25
+ export type FieldCondition = Primitive | OperatorCondition;
26
+ export type Condition = {
27
+ [key: string]: FieldCondition | Condition[] | Condition;
28
+ } & {
29
+ $and?: Condition[];
30
+ $or?: Condition[];
31
+ $not?: Condition;
32
+ };
33
+ export declare function check(condition: Condition, payload: unknown): boolean;
package/dist/index.js ADDED
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ // ==================== Types ====================
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.check = check;
5
+ // ==================== Utils ====================
6
+ function isObject(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ function getByPath(source, path) {
10
+ if (!path)
11
+ return undefined;
12
+ return path.split(".").reduce((acc, key) => {
13
+ return isObject(acc) ? acc[key] : undefined;
14
+ }, source);
15
+ }
16
+ function extractPath(key) {
17
+ const match = key.match(/^\{\{\s*(.+?)\s*\}\}$/);
18
+ return match ? match[1].trim() : key;
19
+ }
20
+ function toRegExp(pattern, flags) {
21
+ return pattern instanceof RegExp
22
+ ? new RegExp(pattern.source, flags !== null && flags !== void 0 ? flags : pattern.flags)
23
+ : new RegExp(pattern, flags);
24
+ }
25
+ function likeToRegExp(pattern, insensitive = false) {
26
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ const regex = `^${escaped.replace(/%/g, ".*").replace(/_/g, ".")}$`;
28
+ return new RegExp(regex, insensitive ? "i" : "");
29
+ }
30
+ const operators = {
31
+ $eq: (actual, expected) => actual === expected,
32
+ $ne: (actual, expected) => actual !== expected,
33
+ $is: (actual, expected) => actual === expected,
34
+ $gt: (actual, expected) => typeof actual === "number" && actual > expected,
35
+ $gte: (actual, expected) => typeof actual === "number" && actual >= expected,
36
+ $lt: (actual, expected) => typeof actual === "number" && actual < expected,
37
+ $lte: (actual, expected) => typeof actual === "number" && actual <= expected,
38
+ $in: (actual, expected) => Array.isArray(expected) && expected.includes(actual),
39
+ $notIn: (actual, expected) => Array.isArray(expected) && !expected.includes(actual),
40
+ $between: (actual, expected) => {
41
+ if (typeof actual !== "number" || !Array.isArray(expected))
42
+ return false;
43
+ const [min, max] = expected;
44
+ return actual >= min && actual <= max;
45
+ },
46
+ $notBetween: (actual, expected) => {
47
+ if (typeof actual !== "number" || !Array.isArray(expected))
48
+ return false;
49
+ const [min, max] = expected;
50
+ return actual < min || actual > max;
51
+ },
52
+ $like: (actual, expected) => typeof actual === "string" && likeToRegExp(expected).test(actual),
53
+ $iLike: (actual, expected) => typeof actual === "string" && likeToRegExp(expected, true).test(actual),
54
+ $startsWith: (actual, expected) => typeof actual === "string" && actual.startsWith(expected),
55
+ $endsWith: (actual, expected) => typeof actual === "string" && actual.endsWith(expected),
56
+ $substring: (actual, expected) => typeof actual === "string" && actual.includes(expected),
57
+ $regexp: (actual, expected) => typeof actual === "string" && toRegExp(expected).test(actual),
58
+ $notRegexp: (actual, expected) => typeof actual === "string" && !toRegExp(expected).test(actual),
59
+ $contains: (actual, expected) => {
60
+ if (typeof actual === "string") {
61
+ return typeof expected === "string" && actual.includes(expected);
62
+ }
63
+ if (Array.isArray(actual)) {
64
+ return Array.isArray(expected)
65
+ ? expected.every(item => actual.includes(item))
66
+ : actual.includes(expected);
67
+ }
68
+ if (isObject(actual) && isObject(expected)) {
69
+ return Object.entries(expected).every(([k, v]) => actual[k] === v);
70
+ }
71
+ return false;
72
+ },
73
+ $overlap: (actual, expected) => {
74
+ if (!Array.isArray(actual) || !Array.isArray(expected))
75
+ return false;
76
+ return expected.some(item => actual.includes(item));
77
+ },
78
+ };
79
+ // ==================== Evaluation ====================
80
+ function evaluateOperators(actual, condition) {
81
+ for (const [op, expected] of Object.entries(condition)) {
82
+ if (op === "$not") {
83
+ if (evaluateField(actual, expected))
84
+ return false;
85
+ continue;
86
+ }
87
+ const fn = operators[op];
88
+ if (!fn)
89
+ continue;
90
+ if (!fn(actual, expected))
91
+ return false;
92
+ }
93
+ return true;
94
+ }
95
+ function evaluateField(actual, condition) {
96
+ if (!isObject(condition)) {
97
+ return actual === condition;
98
+ }
99
+ const hasOperator = Object.keys(condition).some(key => key.startsWith("$"));
100
+ if (!hasOperator) {
101
+ return actual === condition;
102
+ }
103
+ return evaluateOperators(actual, condition);
104
+ }
105
+ // ==================== Logical Operators ====================
106
+ function evaluateLogical(condition, payload) {
107
+ if (condition.$and) {
108
+ if (!condition.$and.every(c => check(c, payload)))
109
+ return false;
110
+ }
111
+ if (condition.$or) {
112
+ if (!condition.$or.some(c => check(c, payload)))
113
+ return false;
114
+ }
115
+ if (condition.$not) {
116
+ if (check(condition.$not, payload))
117
+ return false;
118
+ }
119
+ return true;
120
+ }
121
+ // ==================== Main API ====================
122
+ function check(condition, payload) {
123
+ // Logical operators
124
+ if (!evaluateLogical(condition, payload))
125
+ return false;
126
+ // Field conditions
127
+ for (const [rawKey, rule] of Object.entries(condition)) {
128
+ if (rawKey.startsWith("$"))
129
+ continue;
130
+ const path = extractPath(rawKey);
131
+ const actual = getByPath(payload, path);
132
+ if (!evaluateField(actual, rule))
133
+ return false;
134
+ }
135
+ return true;
136
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@vbnz/conditions",
3
+ "version": "0.0.1",
4
+ "description": "Lightweight condition checker",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "sideEffects": false,
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "keywords": [
22
+ "conditions",
23
+ "rules",
24
+ "typescript"
25
+ ],
26
+ "license": "MIT",
27
+ "devDependencies": {
28
+ "typescript": "^5.6.3"
29
+ }
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,195 @@
1
+ // ==================== Types ====================
2
+
3
+ export type Primitive = string | number | boolean | null | undefined;
4
+
5
+ export type OperatorCondition = {
6
+ $eq?: unknown;
7
+ $ne?: unknown;
8
+ $gt?: number;
9
+ $gte?: number;
10
+ $lt?: number;
11
+ $lte?: number;
12
+ $in?: unknown[];
13
+ $notIn?: unknown[];
14
+ $between?: [number, number];
15
+ $notBetween?: [number, number];
16
+ $like?: string;
17
+ $iLike?: string;
18
+ $startsWith?: string;
19
+ $endsWith?: string;
20
+ $substring?: string;
21
+ $regexp?: string | RegExp;
22
+ $notRegexp?: string | RegExp;
23
+ $contains?: unknown;
24
+ $overlap?: unknown[];
25
+ $is?: unknown;
26
+ $not?: FieldCondition;
27
+ };
28
+
29
+ export type FieldCondition = Primitive | OperatorCondition;
30
+
31
+ export type Condition = {
32
+ [key: string]: FieldCondition | Condition[] | Condition;
33
+ } & {
34
+ $and?: Condition[];
35
+ $or?: Condition[];
36
+ $not?: Condition;
37
+ };
38
+
39
+ // ==================== Utils ====================
40
+
41
+ function isObject(value: unknown): value is Record<string, unknown> {
42
+ return typeof value === "object" && value !== null && !Array.isArray(value);
43
+ }
44
+
45
+ function getByPath(source: unknown, path: string): unknown {
46
+ if (!path) return undefined;
47
+
48
+ return path.split(".").reduce<unknown>((acc, key) => {
49
+ return isObject(acc) ? acc[key] : undefined;
50
+ }, source);
51
+ }
52
+
53
+ function extractPath(key: string): string {
54
+ const match = key.match(/^\{\{\s*(.+?)\s*\}\}$/);
55
+ return match ? match[1].trim() : key;
56
+ }
57
+
58
+ function toRegExp(pattern: string | RegExp, flags?: string): RegExp {
59
+ return pattern instanceof RegExp
60
+ ? new RegExp(pattern.source, flags ?? pattern.flags)
61
+ : new RegExp(pattern, flags);
62
+ }
63
+
64
+ function likeToRegExp(pattern: string, insensitive = false): RegExp {
65
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
+ const regex = `^${escaped.replace(/%/g, ".*").replace(/_/g, ".")}$`;
67
+ return new RegExp(regex, insensitive ? "i" : "");
68
+ }
69
+
70
+ // ==================== Operators ====================
71
+
72
+ type OperatorFn = (actual: unknown, expected: unknown) => boolean;
73
+
74
+ const operators: Record<string, OperatorFn> = {
75
+ $eq: (actual, expected) => actual === expected,
76
+ $ne: (actual, expected) => actual !== expected,
77
+ $is: (actual, expected) => actual === expected,
78
+
79
+ $gt: (actual, expected) => typeof actual === "number" && actual > (expected as number),
80
+ $gte: (actual, expected) => typeof actual === "number" && actual >= (expected as number),
81
+ $lt: (actual, expected) => typeof actual === "number" && actual < (expected as number),
82
+ $lte: (actual, expected) => typeof actual === "number" && actual <= (expected as number),
83
+
84
+ $in: (actual, expected) => Array.isArray(expected) && expected.includes(actual),
85
+ $notIn: (actual, expected) => Array.isArray(expected) && !expected.includes(actual),
86
+
87
+ $between: (actual, expected) => {
88
+ if (typeof actual !== "number" || !Array.isArray(expected)) return false;
89
+ const [min, max] = expected as [number, number];
90
+ return actual >= min && actual <= max;
91
+ },
92
+
93
+ $notBetween: (actual, expected) => {
94
+ if (typeof actual !== "number" || !Array.isArray(expected)) return false;
95
+ const [min, max] = expected as [number, number];
96
+ return actual < min || actual > max;
97
+ },
98
+
99
+ $like: (actual, expected) => typeof actual === "string" && likeToRegExp(expected as string).test(actual),
100
+ $iLike: (actual, expected) => typeof actual === "string" && likeToRegExp(expected as string, true).test(actual),
101
+
102
+ $startsWith: (actual, expected) => typeof actual === "string" && actual.startsWith(expected as string),
103
+ $endsWith: (actual, expected) => typeof actual === "string" && actual.endsWith(expected as string),
104
+ $substring: (actual, expected) => typeof actual === "string" && actual.includes(expected as string),
105
+
106
+ $regexp: (actual, expected) => typeof actual === "string" && toRegExp(expected as string | RegExp).test(actual),
107
+ $notRegexp: (actual, expected) => typeof actual === "string" && !toRegExp(expected as string | RegExp).test(actual),
108
+
109
+ $contains: (actual, expected) => {
110
+ if (typeof actual === "string") {
111
+ return typeof expected === "string" && actual.includes(expected);
112
+ }
113
+ if (Array.isArray(actual)) {
114
+ return Array.isArray(expected)
115
+ ? expected.every(item => actual.includes(item))
116
+ : actual.includes(expected);
117
+ }
118
+ if (isObject(actual) && isObject(expected)) {
119
+ return Object.entries(expected).every(([k, v]) => actual[k] === v);
120
+ }
121
+ return false;
122
+ },
123
+
124
+ $overlap: (actual, expected) => {
125
+ if (!Array.isArray(actual) || !Array.isArray(expected)) return false;
126
+ return expected.some(item => actual.includes(item));
127
+ },
128
+ };
129
+
130
+ // ==================== Evaluation ====================
131
+
132
+ function evaluateOperators(actual: unknown, condition: OperatorCondition): boolean {
133
+ for (const [op, expected] of Object.entries(condition)) {
134
+ if (op === "$not") {
135
+ if (evaluateField(actual, expected as FieldCondition)) return false;
136
+ continue;
137
+ }
138
+
139
+ const fn = operators[op];
140
+ if (!fn) continue;
141
+
142
+ if (!fn(actual, expected)) return false;
143
+ }
144
+ return true;
145
+ }
146
+
147
+ function evaluateField(actual: unknown, condition: FieldCondition): boolean {
148
+ if (!isObject(condition)) {
149
+ return actual === condition;
150
+ }
151
+
152
+ const hasOperator = Object.keys(condition).some(key => key.startsWith("$"));
153
+ if (!hasOperator) {
154
+ return actual === condition;
155
+ }
156
+
157
+ return evaluateOperators(actual, condition as OperatorCondition);
158
+ }
159
+
160
+ // ==================== Logical Operators ====================
161
+
162
+ function evaluateLogical(condition: Condition, payload: unknown): boolean {
163
+ if (condition.$and) {
164
+ if (!condition.$and.every(c => check(c, payload))) return false;
165
+ }
166
+
167
+ if (condition.$or) {
168
+ if (!condition.$or.some(c => check(c, payload))) return false;
169
+ }
170
+
171
+ if (condition.$not) {
172
+ if (check(condition.$not, payload)) return false;
173
+ }
174
+
175
+ return true;
176
+ }
177
+
178
+ // ==================== Main API ====================
179
+
180
+ export function check(condition: Condition, payload: unknown): boolean {
181
+ // Logical operators
182
+ if (!evaluateLogical(condition, payload)) return false;
183
+
184
+ // Field conditions
185
+ for (const [rawKey, rule] of Object.entries(condition)) {
186
+ if (rawKey.startsWith("$")) continue;
187
+
188
+ const path = extractPath(rawKey);
189
+ const actual = getByPath(payload, path);
190
+
191
+ if (!evaluateField(actual, rule as FieldCondition)) return false;
192
+ }
193
+
194
+ return true;
195
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "module": "CommonJS",
5
+ "strict": true,
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["src"]
12
+ }