@inixiative/json-rules 1.1.2 → 1.3.0
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 +352 -226
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +333 -78
- package/dist/index.d.ts +333 -78
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# @inixiative/json-rules
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
A TypeScript-first JSON rules library for:
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
- runtime validation with custom error messages
|
|
6
|
+
- Prisma query planning
|
|
7
|
+
- PostgreSQL `WHERE` generation
|
|
8
|
+
|
|
9
|
+
The same rule AST can be evaluated against in-memory data with `check()`, converted into a Prisma query plan with `toPrisma()`, or compiled into SQL with `toSql()`.
|
|
7
10
|
|
|
8
11
|
## Installation
|
|
9
12
|
|
|
@@ -15,326 +18,449 @@ yarn add @inixiative/json-rules
|
|
|
15
18
|
bun add @inixiative/json-rules
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
## Features
|
|
19
|
-
|
|
20
|
-
- 🎯 **Type-safe**: Full TypeScript support with strict type checking
|
|
21
|
-
- 🔧 **Flexible**: 22 standard operators, 8 array operators, and 8 date operators
|
|
22
|
-
- 🌳 **Composable**: Nest rules with logical operators (all/any) and conditional logic (if-then-else)
|
|
23
|
-
- 📊 **Array validation**: Rich array validation with element-wise conditions
|
|
24
|
-
- 📅 **Date handling**: Comprehensive date comparison with timezone support
|
|
25
|
-
- 🔍 **Path-based access**: Reference values from anywhere in your data structure
|
|
26
|
-
- 💬 **Custom errors**: Every rule supports custom error messages
|
|
27
|
-
|
|
28
|
-
## Installation
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npm install json-rules
|
|
32
|
-
# or
|
|
33
|
-
yarn add json-rules
|
|
34
|
-
# or
|
|
35
|
-
bun add json-rules
|
|
36
|
-
```
|
|
37
|
-
|
|
38
21
|
## Quick Start
|
|
39
22
|
|
|
40
|
-
```
|
|
41
|
-
import { check, Operator } from 'json-rules';
|
|
23
|
+
```ts
|
|
24
|
+
import { check, Operator } from '@inixiative/json-rules';
|
|
42
25
|
|
|
43
|
-
// Simple rule
|
|
44
26
|
const rule = {
|
|
45
27
|
field: 'age',
|
|
46
|
-
operator: Operator.
|
|
28
|
+
operator: Operator.greaterThanEquals,
|
|
47
29
|
value: 18,
|
|
48
|
-
error: 'Must be 18 or older'
|
|
30
|
+
error: 'Must be 18 or older',
|
|
49
31
|
};
|
|
50
32
|
|
|
51
|
-
|
|
52
|
-
|
|
33
|
+
check(rule, { age: 21 }); // true
|
|
34
|
+
check(rule, { age: 16 }); // "Must be 18 or older"
|
|
53
35
|
```
|
|
54
36
|
|
|
37
|
+
## What It Supports
|
|
38
|
+
|
|
39
|
+
- scalar comparisons
|
|
40
|
+
- nested logical conditions with `all` / `any`
|
|
41
|
+
- `if` / `then` / `else`
|
|
42
|
+
- array validation against nested object elements
|
|
43
|
+
- array aggregates — `sum` and `avg` across numeric arrays or relation lists
|
|
44
|
+
- date comparisons with timezone-aware runtime evaluation
|
|
45
|
+
- relative value references via `path`
|
|
46
|
+
- custom error messages on every rule
|
|
47
|
+
- compilation to Prisma and PostgreSQL for supported subsets
|
|
48
|
+
|
|
55
49
|
## Operators
|
|
56
50
|
|
|
57
|
-
###
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- `
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
65
|
-
- `
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
- `
|
|
69
|
-
- `
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- `
|
|
73
|
-
- `
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- `
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
- `
|
|
83
|
-
- `
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- `
|
|
87
|
-
- `
|
|
88
|
-
- `
|
|
89
|
-
- `
|
|
90
|
-
|
|
91
|
-
###
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
- `
|
|
96
|
-
- `
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
- `
|
|
105
|
-
- `
|
|
106
|
-
- `
|
|
107
|
-
- `
|
|
108
|
-
- `
|
|
109
|
-
- `
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
1. **When condition value has no timezone** (e.g., `'2025-01-20'`), it's interpreted in the field's timezone:
|
|
118
|
-
```typescript
|
|
119
|
-
// Field: Jan 20 10:00 AM Sydney time
|
|
120
|
-
{ eventDate: '2025-01-20T10:00:00+11:00' }
|
|
121
|
-
// Condition: on or after Jan 20 (interpreted as Jan 20 in Sydney)
|
|
122
|
-
{ dateOperator: 'onOrAfter', value: '2025-01-20' } // ✓ passes
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
2. **When condition value has timezone** (e.g., `'2025-01-20T00:00:00Z'`), it's used as-is:
|
|
126
|
-
```typescript
|
|
127
|
-
// Condition: after midnight UTC specifically
|
|
128
|
-
{ dateOperator: 'after', value: '2025-01-20T00:00:00Z' }
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
3. **Fields without timezone** are treated as local time (UTC offset 0)
|
|
132
|
-
|
|
133
|
-
## Rule Types
|
|
134
|
-
|
|
135
|
-
### Basic Rule
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
51
|
+
### Field Operators
|
|
52
|
+
|
|
53
|
+
- `equals`
|
|
54
|
+
- `notEquals`
|
|
55
|
+
- `lessThan`
|
|
56
|
+
- `lessThanEquals`
|
|
57
|
+
- `greaterThan`
|
|
58
|
+
- `greaterThanEquals`
|
|
59
|
+
- `contains`
|
|
60
|
+
- `notContains`
|
|
61
|
+
- `in`
|
|
62
|
+
- `notIn`
|
|
63
|
+
- `matches`
|
|
64
|
+
- `notMatches`
|
|
65
|
+
- `between`
|
|
66
|
+
- `notBetween`
|
|
67
|
+
- `isEmpty`
|
|
68
|
+
- `notEmpty`
|
|
69
|
+
- `exists`
|
|
70
|
+
- `notExists`
|
|
71
|
+
- `startsWith`
|
|
72
|
+
- `endsWith`
|
|
73
|
+
|
|
74
|
+
### Array Operators
|
|
75
|
+
|
|
76
|
+
- `all`
|
|
77
|
+
- `any`
|
|
78
|
+
- `none`
|
|
79
|
+
- `atLeast`
|
|
80
|
+
- `atMost`
|
|
81
|
+
- `exactly`
|
|
82
|
+
- `empty`
|
|
83
|
+
- `notEmpty`
|
|
84
|
+
|
|
85
|
+
### Aggregate Operators
|
|
86
|
+
|
|
87
|
+
Used in `aggregate.mode`:
|
|
88
|
+
|
|
89
|
+
- `sum`
|
|
90
|
+
- `avg`
|
|
91
|
+
|
|
92
|
+
Supported comparison operators for aggregate rules: `equals`, `notEquals`, `lessThan`, `lessThanEquals`, `greaterThan`, `greaterThanEquals`, `between`, `notBetween`.
|
|
93
|
+
|
|
94
|
+
### Date Operators
|
|
95
|
+
|
|
96
|
+
- `before`
|
|
97
|
+
- `after`
|
|
98
|
+
- `onOrBefore`
|
|
99
|
+
- `onOrAfter`
|
|
100
|
+
- `between`
|
|
101
|
+
- `notBetween`
|
|
102
|
+
- `dayIn`
|
|
103
|
+
- `dayNotIn`
|
|
104
|
+
|
|
105
|
+
## Rule Shapes
|
|
106
|
+
|
|
107
|
+
### Field Rule
|
|
108
|
+
|
|
109
|
+
```ts
|
|
138
110
|
{
|
|
139
111
|
field: 'status',
|
|
140
|
-
operator: Operator.
|
|
112
|
+
operator: Operator.equals,
|
|
141
113
|
value: 'active'
|
|
142
114
|
}
|
|
143
115
|
```
|
|
144
116
|
|
|
145
|
-
### Logical
|
|
117
|
+
### Logical Rules
|
|
146
118
|
|
|
147
|
-
```
|
|
148
|
-
// All conditions must pass (AND)
|
|
119
|
+
```ts
|
|
149
120
|
{
|
|
150
121
|
all: [
|
|
151
|
-
{ field: 'age', operator: Operator.
|
|
152
|
-
{ field: 'hasLicense', operator: Operator.
|
|
122
|
+
{ field: 'age', operator: Operator.greaterThanEquals, value: 18 },
|
|
123
|
+
{ field: 'hasLicense', operator: Operator.equals, value: true }
|
|
153
124
|
]
|
|
154
125
|
}
|
|
155
126
|
|
|
156
|
-
// At least one must pass (OR)
|
|
157
127
|
{
|
|
158
128
|
any: [
|
|
159
|
-
{ field: 'role', operator: Operator.
|
|
160
|
-
{ field: 'isOwner', operator: Operator.
|
|
129
|
+
{ field: 'role', operator: Operator.equals, value: 'admin' },
|
|
130
|
+
{ field: 'isOwner', operator: Operator.equals, value: true }
|
|
161
131
|
]
|
|
162
132
|
}
|
|
163
133
|
```
|
|
164
134
|
|
|
165
|
-
### Conditional
|
|
135
|
+
### Conditional Rule
|
|
166
136
|
|
|
167
|
-
```
|
|
137
|
+
```ts
|
|
168
138
|
{
|
|
169
|
-
if: { field: 'type', operator: Operator.
|
|
139
|
+
if: { field: 'type', operator: Operator.equals, value: 'premium' },
|
|
170
140
|
then: { field: 'discount', operator: Operator.greaterThan, value: 0 },
|
|
171
|
-
else: { field: 'discount', operator: Operator.
|
|
141
|
+
else: { field: 'discount', operator: Operator.equals, value: 0 }
|
|
172
142
|
}
|
|
173
143
|
```
|
|
174
144
|
|
|
175
|
-
### Array
|
|
145
|
+
### Array Rule
|
|
176
146
|
|
|
177
|
-
```
|
|
147
|
+
```ts
|
|
178
148
|
{
|
|
179
149
|
field: 'orders',
|
|
180
150
|
arrayOperator: ArrayOperator.all,
|
|
181
151
|
condition: {
|
|
182
152
|
field: 'total',
|
|
183
|
-
operator: Operator.
|
|
184
|
-
|
|
153
|
+
operator: Operator.lessThanEquals,
|
|
154
|
+
path: '$.maxBudget'
|
|
185
155
|
}
|
|
186
156
|
}
|
|
187
157
|
```
|
|
188
158
|
|
|
189
|
-
###
|
|
159
|
+
### Aggregate Rule
|
|
160
|
+
|
|
161
|
+
Computes `sum` or `avg` of an array and compares the result to a value.
|
|
190
162
|
|
|
191
|
-
```
|
|
163
|
+
```ts
|
|
164
|
+
// Primitive numeric array
|
|
165
|
+
{
|
|
166
|
+
field: 'scores',
|
|
167
|
+
aggregate: { mode: 'avg' },
|
|
168
|
+
operator: Operator.greaterThanEquals,
|
|
169
|
+
value: 80
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Object array — aggregate.field selects the numeric property per element
|
|
173
|
+
{
|
|
174
|
+
field: 'orders',
|
|
175
|
+
aggregate: { mode: 'sum', field: 'total' },
|
|
176
|
+
operator: Operator.greaterThan,
|
|
177
|
+
value: 1000
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Empty-array semantics: `sum([]) = 0`, `avg([]) = null` (comparison fails).
|
|
182
|
+
|
|
183
|
+
### Date Rule
|
|
184
|
+
|
|
185
|
+
```ts
|
|
192
186
|
{
|
|
193
187
|
field: 'expiryDate',
|
|
194
188
|
dateOperator: DateOperator.after,
|
|
195
|
-
value: '
|
|
189
|
+
value: '2026-01-01'
|
|
196
190
|
}
|
|
197
191
|
```
|
|
198
192
|
|
|
199
|
-
##
|
|
193
|
+
## Path Semantics
|
|
194
|
+
|
|
195
|
+
`path` lets a rule resolve its comparison value from somewhere other than `value`.
|
|
200
196
|
|
|
201
|
-
###
|
|
197
|
+
### Root Context Reference
|
|
202
198
|
|
|
203
|
-
|
|
199
|
+
In runtime validation, a plain path is resolved from the root context:
|
|
204
200
|
|
|
205
|
-
```
|
|
201
|
+
```ts
|
|
206
202
|
{
|
|
207
203
|
field: 'confirmPassword',
|
|
208
|
-
operator: Operator.
|
|
209
|
-
path: 'password'
|
|
204
|
+
operator: Operator.equals,
|
|
205
|
+
path: 'password'
|
|
210
206
|
}
|
|
211
207
|
```
|
|
212
208
|
|
|
213
|
-
### Array Element
|
|
209
|
+
### Current Array Element Reference
|
|
214
210
|
|
|
215
|
-
|
|
211
|
+
Inside array conditions, `$.` means "read from the current element":
|
|
216
212
|
|
|
217
|
-
```
|
|
213
|
+
```ts
|
|
218
214
|
{
|
|
219
|
-
field: '
|
|
215
|
+
field: 'orders',
|
|
220
216
|
arrayOperator: ArrayOperator.all,
|
|
221
217
|
condition: {
|
|
222
|
-
field: '
|
|
223
|
-
operator: Operator.
|
|
224
|
-
path: '$.
|
|
218
|
+
field: 'total',
|
|
219
|
+
operator: Operator.lessThanEquals,
|
|
220
|
+
path: '$.maxBudget'
|
|
225
221
|
}
|
|
226
222
|
}
|
|
227
223
|
```
|
|
228
224
|
|
|
229
|
-
|
|
225
|
+
## Runtime Validation
|
|
230
226
|
|
|
231
|
-
|
|
227
|
+
`check()` evaluates a rule against data and returns:
|
|
232
228
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
field: 'email',
|
|
236
|
-
operator: Operator.match,
|
|
237
|
-
value: /^[^@]+@[^@]+\.[^@]+$/,
|
|
238
|
-
error: 'Please enter a valid email address'
|
|
239
|
-
}
|
|
240
|
-
```
|
|
229
|
+
- `true` when the rule passes
|
|
230
|
+
- a string when the rule fails
|
|
241
231
|
|
|
242
|
-
|
|
232
|
+
```ts
|
|
233
|
+
import { ArrayOperator, check, Operator } from '@inixiative/json-rules';
|
|
243
234
|
|
|
244
|
-
```typescript
|
|
245
235
|
const rule = {
|
|
246
236
|
all: [
|
|
247
|
-
|
|
248
|
-
{ field: 'status', operator: Operator.equal, value: 'active' },
|
|
249
|
-
|
|
250
|
-
// Age requirement
|
|
251
|
-
{ field: 'age', operator: Operator.between, value: [18, 65] },
|
|
252
|
-
|
|
253
|
-
// Must have at least one verified email
|
|
237
|
+
{ field: 'status', operator: Operator.equals, value: 'active' },
|
|
254
238
|
{
|
|
255
|
-
field: '
|
|
256
|
-
arrayOperator: ArrayOperator.
|
|
257
|
-
|
|
239
|
+
field: 'orders',
|
|
240
|
+
arrayOperator: ArrayOperator.atLeast,
|
|
241
|
+
count: 2,
|
|
242
|
+
condition: { field: 'status', operator: Operator.equals, value: 'completed' },
|
|
258
243
|
},
|
|
259
|
-
|
|
260
|
-
// Conditional premium features
|
|
261
|
-
{
|
|
262
|
-
if: { field: 'subscription', operator: Operator.equal, value: 'premium' },
|
|
263
|
-
then: {
|
|
264
|
-
field: 'features',
|
|
265
|
-
arrayOperator: ArrayOperator.all,
|
|
266
|
-
condition: { field: 'enabled', operator: Operator.equal, value: true }
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
]
|
|
244
|
+
],
|
|
270
245
|
};
|
|
271
246
|
|
|
272
|
-
|
|
247
|
+
check(rule, {
|
|
273
248
|
status: 'active',
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
{
|
|
277
|
-
{
|
|
249
|
+
orders: [
|
|
250
|
+
{ status: 'completed' },
|
|
251
|
+
{ status: 'pending' },
|
|
252
|
+
{ status: 'completed' },
|
|
278
253
|
],
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
254
|
+
}); // true
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Custom Errors
|
|
258
|
+
|
|
259
|
+
Every rule can define its own error:
|
|
285
260
|
|
|
286
|
-
|
|
261
|
+
```ts
|
|
262
|
+
{
|
|
263
|
+
field: 'email',
|
|
264
|
+
operator: Operator.matches,
|
|
265
|
+
value: /^[^@]+@[^@]+\.[^@]+$/,
|
|
266
|
+
error: 'Please enter a valid email address'
|
|
267
|
+
}
|
|
287
268
|
```
|
|
288
269
|
|
|
289
|
-
##
|
|
270
|
+
## Prisma Query Planning
|
|
290
271
|
|
|
291
|
-
|
|
272
|
+
`toPrisma()` converts a rule into a Prisma query plan.
|
|
292
273
|
|
|
293
|
-
|
|
274
|
+
```ts
|
|
275
|
+
import { Operator, toPrisma } from '@inixiative/json-rules';
|
|
294
276
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
277
|
+
const plan = toPrisma({
|
|
278
|
+
field: 'status',
|
|
279
|
+
operator: Operator.equals,
|
|
280
|
+
value: 'active',
|
|
281
|
+
});
|
|
299
282
|
|
|
300
|
-
|
|
283
|
+
// plan.steps => [{ operation: 'where', where: { status: { equals: 'active' } } }]
|
|
284
|
+
```
|
|
301
285
|
|
|
302
|
-
|
|
303
|
-
|
|
286
|
+
Aggregate relation filters (`sum`, `avg`) and count-based filters (`atLeast`, `atMost`, `exactly`) can produce multi-step plans. Use `executePrismaQueryPlan()` to resolve `groupBy` step references before passing the final `where` into Prisma.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import {
|
|
290
|
+
ArrayOperator,
|
|
291
|
+
Operator,
|
|
292
|
+
executePrismaQueryPlan,
|
|
293
|
+
toPrisma,
|
|
294
|
+
} from '@inixiative/json-rules';
|
|
295
|
+
|
|
296
|
+
const plan = toPrisma(
|
|
297
|
+
{
|
|
298
|
+
field: 'posts',
|
|
299
|
+
arrayOperator: ArrayOperator.atLeast,
|
|
300
|
+
count: 3,
|
|
301
|
+
condition: {
|
|
302
|
+
field: 'published',
|
|
303
|
+
operator: Operator.equals,
|
|
304
|
+
value: true,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{ map, model: 'User' },
|
|
308
|
+
);
|
|
304
309
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
value?: any;
|
|
309
|
-
path?: string;
|
|
310
|
-
error?: string;
|
|
311
|
-
};
|
|
310
|
+
const where = await executePrismaQueryPlan(plan, { post: prisma.post });
|
|
311
|
+
await prisma.user.findMany({ where });
|
|
312
|
+
```
|
|
312
313
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
314
|
+
Aggregate rules on relation lists work the same way:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const plan = toPrisma(
|
|
318
|
+
{
|
|
319
|
+
field: 'orders',
|
|
320
|
+
aggregate: { mode: 'sum', field: 'total' },
|
|
321
|
+
operator: Operator.greaterThan,
|
|
322
|
+
value: 1000,
|
|
323
|
+
},
|
|
324
|
+
{ map, model: 'User' },
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const where = await executePrismaQueryPlan(plan, { order: prisma.order });
|
|
328
|
+
await prisma.user.findMany({ where }); // users whose orders sum to more than 1000
|
|
329
|
+
```
|
|
320
330
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
331
|
+
## PostgreSQL SQL Generation
|
|
332
|
+
|
|
333
|
+
`toSql()` converts a rule into a parameterized PostgreSQL `WHERE` clause.
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { Operator, toSql } from '@inixiative/json-rules';
|
|
337
|
+
|
|
338
|
+
const result = toSql({
|
|
339
|
+
field: 'status',
|
|
340
|
+
operator: Operator.equals,
|
|
341
|
+
value: 'active',
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// {
|
|
345
|
+
// sql: '"status" = $1',
|
|
346
|
+
// params: ['active'],
|
|
347
|
+
// joins: []
|
|
348
|
+
// }
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
With a field map and model, `toSql()` can generate `LEFT JOIN`s for relation traversal:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
const result = toSql(
|
|
355
|
+
{ field: 'author.email', operator: Operator.equals, value: 'a@b.com' },
|
|
356
|
+
{ map, model: 'Post', alias: 't0' },
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
// result.sql => '"t1"."email" = $1'
|
|
360
|
+
// result.joins => ['LEFT JOIN "User" AS "t1" ON "t1"."id" = "t0"."authorId"']
|
|
328
361
|
```
|
|
329
362
|
|
|
363
|
+
## Backend Support Matrix
|
|
364
|
+
|
|
365
|
+
Not every backend supports every rule shape.
|
|
366
|
+
|
|
367
|
+
| Capability | `check()` | `toPrisma()` | `toSql()` |
|
|
368
|
+
| --- | --- | --- | --- |
|
|
369
|
+
| Field operators | Yes | Most | Yes |
|
|
370
|
+
| `matches` / `notMatches` | Yes | No | Yes |
|
|
371
|
+
| Logical operators | Yes | Yes | Yes |
|
|
372
|
+
| Array `all` / `any` / `none` | Yes | Yes | No |
|
|
373
|
+
| Array `atLeast` / `atMost` / `exactly` | Yes | Yes, with `map` + `model` | No |
|
|
374
|
+
| Array `empty` / `notEmpty` | Yes | Yes | Yes |
|
|
375
|
+
| Aggregate `sum` / `avg` — primitive or object array | Yes | No | Yes |
|
|
376
|
+
| Aggregate `sum` / `avg` — relation list | Yes | Yes, with `map` + `model` | No |
|
|
377
|
+
| Date comparisons | Yes | Most | Yes |
|
|
378
|
+
| `dayIn` / `dayNotIn` | Yes | No | Yes |
|
|
379
|
+
| `path: '$.field'` current-element / same-row refs | Yes | No | Yes |
|
|
380
|
+
|
|
381
|
+
### Prisma Limitations
|
|
382
|
+
|
|
383
|
+
- `matches` and `notMatches` are not supported by Prisma output
|
|
384
|
+
- `dayIn` and `dayNotIn` are not supported by Prisma output
|
|
385
|
+
- `path: '$.field'` column-to-column comparisons are not supported by Prisma `WHERE`
|
|
386
|
+
- count-based and aggregate relation operators require `{ map, model }`
|
|
387
|
+
- aggregate rules with `notBetween` are not supported by Prisma output
|
|
388
|
+
- aggregate rules on JSON/native stored arrays are not supported by Prisma — use `toSql()` or `check()` for those
|
|
389
|
+
|
|
390
|
+
### SQL Limitations
|
|
391
|
+
|
|
392
|
+
- complex array element operators are not supported in SQL output:
|
|
393
|
+
- `all`
|
|
394
|
+
- `any`
|
|
395
|
+
- `none`
|
|
396
|
+
- `atLeast`
|
|
397
|
+
- `atMost`
|
|
398
|
+
- `exactly`
|
|
399
|
+
- `toSql()` generates `WHERE` fragments and `LEFT JOIN`s, not complete queries
|
|
400
|
+
|
|
401
|
+
## TypeScript Types
|
|
402
|
+
|
|
403
|
+
The public rule types are generic over comparison payloads:
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
type Condition<TRuleValue = RuleValue, TDateValue = DateRuleValue> =
|
|
407
|
+
| Rule<TRuleValue>
|
|
408
|
+
| AggregateRule
|
|
409
|
+
| ArrayRule<TRuleValue, TDateValue>
|
|
410
|
+
| DateRule<TDateValue>
|
|
411
|
+
| All<TRuleValue, TDateValue>
|
|
412
|
+
| Any<TRuleValue, TDateValue>
|
|
413
|
+
| IfThenElse<TRuleValue, TDateValue>
|
|
414
|
+
| boolean;
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Useful exports:
|
|
418
|
+
|
|
419
|
+
- `check`
|
|
420
|
+
- `toPrisma`
|
|
421
|
+
- `executePrismaQueryPlan`
|
|
422
|
+
- `toSql`
|
|
423
|
+
- `validateRule`
|
|
424
|
+
- `assertValidRule`
|
|
425
|
+
- `Operator`
|
|
426
|
+
- `ArrayOperator`
|
|
427
|
+
- `DateOperator`
|
|
428
|
+
- `Condition`
|
|
429
|
+
- `StrictCondition`
|
|
430
|
+
- `Rule`
|
|
431
|
+
- `AggregateRule`
|
|
432
|
+
- `AggregateMode`
|
|
433
|
+
- `ArrayRule`
|
|
434
|
+
- `DateRule`
|
|
435
|
+
|
|
330
436
|
## Error Handling
|
|
331
437
|
|
|
332
|
-
The
|
|
333
|
-
|
|
334
|
-
-
|
|
335
|
-
-
|
|
336
|
-
-
|
|
438
|
+
The library throws when a rule is structurally invalid, for example:
|
|
439
|
+
|
|
440
|
+
- array operators used against non-arrays
|
|
441
|
+
- missing `count` for count-based array rules
|
|
442
|
+
- invalid date values
|
|
443
|
+
- unsupported backend translations
|
|
444
|
+
|
|
445
|
+
It returns string errors only from runtime `check()`.
|
|
446
|
+
|
|
447
|
+
If rules come from JSON, a database, an API, or an editor, validate them first:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
import { assertValidRule, validateRule } from '@inixiative/json-rules';
|
|
451
|
+
|
|
452
|
+
const result = validateRule(rule, { target: 'check' });
|
|
453
|
+
if (!result.ok) {
|
|
454
|
+
console.error(result.errors);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
assertValidRule(rule, { target: 'toPrisma' });
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Examples
|
|
461
|
+
|
|
462
|
+
See [`examples/basic-validation.ts`](./examples/basic-validation.ts), [`examples/array-operations.ts`](./examples/array-operations.ts), [`examples/aggregate-rules.ts`](./examples/aggregate-rules.ts), [`examples/date-operations.ts`](./examples/date-operations.ts), and [`examples/advanced-features.ts`](./examples/advanced-features.ts).
|
|
337
463
|
|
|
338
464
|
## License
|
|
339
465
|
|
|
340
|
-
MIT
|
|
466
|
+
MIT
|