@uql/core 3.11.1 → 3.12.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/CHANGELOG.md CHANGED
@@ -3,9 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [3.11.1](https://github.com/rogerpadilla/uql/compare/@uql/core@3.11.0...@uql/core@3.11.1) (2026-02-26)
6
+ ## [3.12.1](https://github.com/rogerpadilla/uql/compare/@uql/core@3.12.0...@uql/core@3.12.1) (2026-03-05)
7
7
 
8
- **Note:** Version bump only for package @uql/core
8
+
9
+ ### Bug Fixes
10
+
11
+ * Make JSONB `$ne` operator null-safe by using `IS DISTINCT FROM` / `IS NOT` and refine query map type definitions. ([2bb2023](https://github.com/rogerpadilla/uql/commit/2bb202310d5a1a7fa2bc0ff7948c892afa4ba4f2))
12
+
13
+
14
+ ### Features
15
+
16
+ * Enhance `QueryWhereMap` type for improved JSONB dot-notation and relation filtering, eliminating `as any` casts in tests. ([2092e11](https://github.com/rogerpadilla/uql/commit/2092e113d858130941a0d0b2807026b73c852946))
9
17
 
10
18
 
11
19
 
@@ -17,6 +25,54 @@ All notable changes to this project will be documented in this file. Please add
17
25
 
18
26
  date format is [yyyy-mm-dd]
19
27
 
28
+ ## [3.12.1] - 2026-03-05
29
+ ### Bug Fixes
30
+ - **Null-Safe JSONB `$ne`**: JSONB dot-notation `$ne` now uses null-safe operators (`IS DISTINCT FROM` on PostgreSQL, `IS NOT` on SQLite) so that absent keys (which return SQL `NULL`) are correctly included in results. Previously, `{ 'settings.isArchived': { $ne: true } }` would silently exclude rows where the key didn't exist.
31
+ - **JSONB `$eq`/`$ne` with `null`**: `$eq: null` and `$ne: null` on JSONB paths now correctly generate `IS NULL` / `IS NOT NULL` instead of `= NULL` / `<> NULL`.
32
+
33
+ ### Improvements & Refactoring
34
+ - **`QueryWhereMap` Type Safety**: Replaced the overly permissive `Record<string, ...>` catch-all with explicit typed unions: template literal `` `${string}.${string}` `` for dot-paths, `RelationKey<E>` for relation filtering, and `JsonFieldPaths<E>` for IDE autocompletion.
35
+ - **`DeepJsonKeys` Recursive Type**: `JsonFieldPaths<E>` now derives dot-notation paths up to 5 levels deep (previously only 1 level), enabling autocompletion for nested JSONB structures like `'kind.theme.color'`.
36
+ - **DRY `compare()` Signature**: Simplified `compare()` from `<E, K extends keyof QueryWhereMap<E>>(key: K, val: QueryWhereMap<E>[K])` to `<E>(key: string, val: unknown)` across all dialect overrides, removing redundant generic constraints.
37
+ - **DRY SQLite Config**: SQLite's `getBaseJsonConfig()` now spreads from `super` instead of duplicating 4 identical fields.
38
+
39
+ ### Test Coverage
40
+ - Removed ~20 `as any` casts from tests (now unnecessary with improved types). Added null-safe `$ne` tests across all dialects. Total: **1,563 tests passing**.
41
+
42
+ ## [3.12.0] - 2026-03-05
43
+ ### New Features
44
+ - **JSONB Dot-Notation Operators**: Filter by nested JSON field paths directly in `$where` with full operator support (`$eq`, `$ne`, `$gt`, `$lt`, `$like`, `$ilike`, `$in`, `$nin`, `$regex`, etc.). Works across PostgreSQL, MySQL, and SQLite.
45
+ ```ts
46
+ await querier.findMany(User, {
47
+ $where: { 'settings.isArchived': { $ne: true } },
48
+ });
49
+ ```
50
+ - **Relation Filtering**: Filter by ManyToMany and OneToMany relations using automatic EXISTS subqueries. No more manual `raw()` joins.
51
+ ```ts
52
+ await querier.findMany(Item, {
53
+ $where: { tags: { name: 'important' } },
54
+ });
55
+ ```
56
+ - **`Json<T>` Marker Type**: Wrap JSONB field types with `Json<T>` to ensure they are classified as `FieldKey` (not `RelationKey`), enabling type-safe usage in `$where`, `$select`, and `$sort`.
57
+ ```ts
58
+ @Field({ type: 'jsonb' })
59
+ settings?: Json<{ isArchived?: boolean }>;
60
+ ```
61
+ - **`JsonFieldPaths<E>` Autocompletion**: Template literal type that derives valid dot-notation paths from `Json<T>` fields (e.g., `'kind.public' | 'kind.private'`). Provides IDE autocompletion for JSONB `$where` queries without restricting arbitrary string paths.
62
+
63
+ ### Bug Fixes
64
+ - **raw() String Prefix Fix**: String-based `raw()` values in `$and`/`$or` are no longer incorrectly table-prefixed (e.g., `raw("kind IS NOT NULL")` previously produced `resource.kind IS NOT NULL` instead of `kind IS NOT NULL`).
65
+
66
+ ### Improvements & Refactoring
67
+ - **DRY JSON Config**: Extracted `getBaseJsonConfig()` in each dialect — `$elemMatch` and dot-notation now compose from a single config source, eliminating ~20 lines of duplication.
68
+ - **Extracted `normalizeWhereValue()`**: Deduplicated the `Array→$in / object→passthrough / scalar→$eq` normalization used by both regular field and JSON path comparisons.
69
+ - **Cleaner Dot-Notation Detection**: Uses `indexOf`+`slice` instead of two `split('.')` calls for efficient dot-path parsing.
70
+ - **Relation Safety Guard**: `compareRelation()` now throws a descriptive `TypeError` if `rel.references` is missing, instead of a cryptic undefined crash.
71
+ - **TypeScript 6 Compatibility**: Fixed `QueryWhereMap` circular type reference and expanded `QueryWhereOptions.clause` union.
72
+
73
+ ### Test Coverage
74
+ - **46 new tests** across 3 dialects (base, PostgreSQL, SQLite) covering all new features and edge cases. Total: **1561 tests passing**.
75
+
20
76
  ## [3.11.1] - 2026-02-26
21
77
  ### Improvements
22
78
  - **Expanded ColumnType Aliases**: Added `integer`, `tinyint`, `bool`, `datetime`, and `smallserial` as first-class `ColumnType` values (aliases for `int`, `boolean`, `timestamp`, `smallserial`, and `serial` respectively). Users can now use standard SQL keywords interchangeably (e.g., `integer` or `int`, `bool` or `boolean`, `datetime` or `timestamp`).
package/README.md CHANGED
@@ -112,7 +112,7 @@ UQL separates the **intent** of your data from its **storage**. Both properties
112
112
  externalId?: string;
113
113
 
114
114
  @Field({ type: 'json' }) // → JSONB (Postgres), JSON (MySQL), TEXT (SQLite)
115
- metadata?: Record<string, unknown>;
115
+ metadata?: Json<{ theme?: string }>;
116
116
 
117
117
  // Logical types with constraints - portable with control
118
118
  @Field({ type: 'varchar', length: 500 })
@@ -131,7 +131,7 @@ statusCode?: number;
131
131
 
132
132
  ```ts
133
133
  import { v7 as uuidv7 } from 'uuid';
134
- import { Entity, Id, Field, OneToOne, OneToMany, ManyToOne, ManyToMany, type Relation } from '@uql/core';
134
+ import { Entity, Id, Field, OneToOne, OneToMany, ManyToOne, ManyToMany, type Relation, type Json } from '@uql/core';
135
135
 
136
136
  @Entity()
137
137
  export class User {
@@ -314,6 +314,38 @@ export class Item {
314
314
 
315
315
  &nbsp;
316
316
 
317
+ ### JSONB Operators & Relation Filtering
318
+
319
+ Query nested JSON fields using **type-safe dot-notation** with full operator support. Wrap fields with `Json<T>` to get IDE autocompletion for valid paths. UQL generates the correct SQL for each dialect.
320
+
321
+ ```ts
322
+ // Filter by nested JSONB field paths
323
+ const items = await querier.findMany(Company, {
324
+ $where: {
325
+ 'settings.isArchived': { $ne: true },
326
+ 'settings.priority': { $gte: 5 },
327
+ },
328
+ });
329
+ ```
330
+
331
+ **PostgreSQL:** `WHERE ("settings"->>'isArchived') IS DISTINCT FROM $1 AND (("settings"->>'priority'))::numeric >= $2`
332
+ **SQLite:** `WHERE json_extract("settings", '$.isArchived') IS NOT ? AND CAST(json_extract("settings", '$.priority') AS REAL) >= ?`
333
+
334
+ Filter parent entities by their **ManyToMany** or **OneToMany** relations using automatic EXISTS subqueries:
335
+
336
+ ```ts
337
+ // Find posts that have a tag named 'typescript'
338
+ const posts = await querier.findMany(Post, {
339
+ $where: { tags: { name: 'typescript' } },
340
+ });
341
+ ```
342
+
343
+ **PostgreSQL:** `WHERE EXISTS (SELECT 1 FROM "PostTag" WHERE "PostTag"."postId" = "Post"."id" AND "PostTag"."tagId" IN (SELECT "Tag"."id" FROM "Tag" WHERE "Tag"."name" = $1))`
344
+
345
+ > **Pro Tip**: Wrap JSONB field types with `Json<T>` (e.g., `settings?: Json<{ isArchived?: boolean }>`) to get IDE autocompletion for dot-notation paths.
346
+
347
+ &nbsp;
348
+
317
349
  ### Thread-Safe Transactions
318
350
 
319
351
  UQL is one of the few ORMs with a **centralized serialization engine**. Transactions are guaranteed to be race-condition free.