@inixiative/json-rules 2.19.4 → 2.19.6

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 CHANGED
@@ -101,7 +101,10 @@ Supported comparison operators for aggregate rules: `equals`, `notEquals`, `less
101
101
  - `after`
102
102
  - `onOrBefore`
103
103
  - `onOrAfter`
104
+ - `notBefore`
105
+ - `notAfter`
104
106
  - `within`
107
+ - `notWithin`
105
108
  - `between`
106
109
  - `notBetween`
107
110
  - `dayIn`
@@ -220,13 +223,18 @@ absolute date. Magnitudes are always **positive** — direction lives in the key
220
223
  Units are dayjs words: `day`, `week`, `isoWeek`, `month`, `quarter`, `year`,
221
224
  `hour`, `minute`, `second`.
222
225
 
223
- **Point expressions** — pair with `before` / `after` / `onOrBefore` / `onOrAfter`,
226
+ **Point expressions** — pair with `before` / `after` / `onOrBefore` / `onOrAfter` /
227
+ `notBefore` / `notAfter`,
224
228
  or as `between` endpoints:
225
229
 
226
230
  ```ts
227
231
  // "more than 30 days ago"
228
232
  { field: 'completedAt', dateOperator: DateOperator.before, value: { ago: { days: 30 } } }
229
233
 
234
+ // "not since 30 days ago" — on/before the point OR never (notAfter is the null-carrying
235
+ // complement of after; onOrBefore is positive and skips NULL). Takes a literal date too.
236
+ { field: 'lastLoginAt', dateOperator: DateOperator.notAfter, value: { ago: { days: 30 } } }
237
+
230
238
  // "within the next 2 months"
231
239
  { field: 'dueAt', dateOperator: DateOperator.after, value: { ahead: { months: 2 } } }
232
240
 
@@ -234,7 +242,7 @@ or as `between` endpoints:
234
242
  { field: 'completedAt', dateOperator: DateOperator.before, value: { end: { last: 'month' } } }
235
243
  ```
236
244
 
237
- **Range expressions** — pair with the `within` operator:
245
+ **Range expressions** — pair with `within` / `notWithin`:
238
246
 
239
247
  ```ts
240
248
  // "this month"
@@ -245,6 +253,10 @@ or as `between` endpoints:
245
253
 
246
254
  // rolling window: "within the last 30 days" → [now - 30d, now]
247
255
  { field: 'completedAt', dateOperator: DateOperator.within, value: { ago: { days: 30 } } }
256
+
257
+ // its complement: "not in the last 30 days" — a never-set date is not in the window, so
258
+ // NULL matches (see NULL Semantics). The dormancy rule, in one leaf.
259
+ { field: 'lastLoginAt', dateOperator: DateOperator.notWithin, value: { ago: { days: 30 } } }
248
260
  ```
249
261
 
250
262
  A **bare period** with `before` / `after` resolves to the only sensible edge —
@@ -531,7 +543,7 @@ Without `{ map, model }`, or on an entry that doesn't declare it, the bare
531
543
  Date rules follow the same split. The positive operators answer non-match on both
532
544
  rails — a bare boundary is not something a NULL column satisfies, so `check()`
533
545
  reports the rule's ordinary non-match (honoring `error`) and the compilers keep the
534
- bare `<` / `BETWEEN`. The negative-flavored ones (`notBetween`, `dayNotIn`) follow
546
+ bare `<` / `BETWEEN`. The negative-flavored ones (`notBefore`, `notAfter`, `notWithin`, `notBetween`, `dayNotIn`) follow
535
547
  the negation ruling instead: a never-set date is not in the range, so a null column
536
548
  MATCHES, and the compilers carry the `IS NULL` arm. To match never-seen rows under a
537
549
  positive operator, ask for them:
@@ -540,6 +552,8 @@ positive operator, ask for them:
540
552
  | Rule | `check()` on `{ col: null }` | `toSql()` | `toPrisma()` |
541
553
  | --- | --- | --- | --- |
542
554
  | positive `dateOperator` (`before`, `between`, `dayIn`, …) | no match | `col < $1` (NULL never satisfies) | `{ col: { lt: … } }` |
555
+ | `notAfter X` / `notBefore X` | matches | `(col <= $1 OR col IS NULL)` / `(col >= $1 OR col IS NULL)` | `{ OR: [{ col: { lte } }, { col: { equals: null } }] }` (nullable column) |
556
+ | `notWithin { ago: { days: 30 } }` | matches | `(col NOT BETWEEN $1 AND $2 OR col IS NULL)` | `{ OR: [{ col: { NOT: { gte, lte } } }, { col: { equals: null } }] }` (nullable column) |
543
557
  | `notBetween` | matches | `(col NOT BETWEEN $1 AND $2 OR col IS NULL)` | `{ OR: [{ col: { NOT: … } }, { col: { equals: null } }] }` (nullable column, same field-map licensing as above) |
544
558
  | `dayNotIn` | matches | `(EXTRACT(DOW FROM col) <> ALL($1) OR col IS NULL)` | — (no Prisma output) |
545
559
  | `notExists` OR `before` | matches via the first arm | `(col IS NULL OR col < $1)` | `{ OR: [{ col: { equals: null } }, { col: { lt: … } }] }` |