turbine-orm 0.30.0 → 0.31.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.
@@ -36,17 +36,48 @@ export function isUnmatchedPlainObject(value) {
36
36
  const proto = Object.getPrototypeOf(value);
37
37
  return proto === Object.prototype || proto === null;
38
38
  }
39
+ /**
40
+ * Operator keys that accept a {@link ColumnRef} (`{ col: 'otherField' }`)
41
+ * value for column-to-column comparison. `in`/`notIn` and the LIKE operators
42
+ * take values only.
43
+ */
44
+ export const COLUMN_REF_OPERATORS = new Set(['equals', 'not', 'gt', 'gte', 'lt', 'lte']);
45
+ /**
46
+ * Check if an operator value is a column reference: a plain object whose ONLY
47
+ * key is `col` with a string value. Anything else (extra keys, non-string
48
+ * `col`) is treated as a plain value so JSON payloads that merely contain a
49
+ * `col` property keep their equality meaning.
50
+ */
51
+ export function isColumnRef(value) {
52
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || value instanceof Date)
53
+ return false;
54
+ const keys = Object.keys(value);
55
+ return keys.length === 1 && keys[0] === 'col' && typeof value.col === 'string';
56
+ }
39
57
  /**
40
58
  * Fingerprint the SHAPE of a where-operator object. Null-valued `equals` /
41
59
  * `not` compile to parameterless `IS NULL` / `IS NOT NULL` (different SQL, no
42
60
  * param pushed), so null-ness is part of the shape — without it a cache entry
43
61
  * warmed by `{ not: 5 }` would serve `{ not: null }` with a desynced param list.
62
+ *
63
+ * Column references ({@link ColumnRef}) compile the referenced column into the
64
+ * SQL TEXT (no param bound), so the referenced field name is part of the shape
65
+ *: `{ equals: { col: 'a' } }` and `{ equals: { col: 'b' } }` must never share
66
+ * a cache entry. The name is JSON-encoded so exotic field names cannot collide
67
+ * with other fingerprint tokens.
44
68
  */
45
69
  export function fingerprintOperatorShape(value) {
46
70
  const obj = value;
47
71
  const opKeys = Object.keys(obj)
48
72
  .filter((k) => k !== 'mode')
49
- .map((k) => ((k === 'equals' || k === 'not') && obj[k] === null ? `${k}:null` : k))
73
+ .map((k) => {
74
+ const v = obj[k];
75
+ if ((k === 'equals' || k === 'not') && v === null)
76
+ return `${k}:null`;
77
+ if (COLUMN_REF_OPERATORS.has(k) && isColumnRef(v))
78
+ return `${k}:col(${JSON.stringify(v.col)})`;
79
+ return k;
80
+ })
50
81
  .sort();
51
82
  const modeStr = value.mode === 'insensitive' ? ':i' : '';
52
83
  return `op(${opKeys.join(',')}${modeStr})`;
@@ -249,6 +280,20 @@ export function isVectorOrderBy(value) {
249
280
  export function isOrderBySpec(value) {
250
281
  return typeof value === 'object' && value !== null && !Array.isArray(value) && 'sort' in value;
251
282
  }
283
+ /**
284
+ * Check if an orderBy value is a JSON-path ordering: `{ path: [...] }` with an
285
+ * ARRAY path. The array requirement disambiguates from relation orderBy values
286
+ * (whose entries are directions/specs keyed by target column: a target column
287
+ * literally named `path` maps to a string direction, never an array), and the
288
+ * `distance`/`sort` exclusions keep vector and spec shapes out.
289
+ */
290
+ export function isJsonPathOrderBy(value) {
291
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
292
+ return false;
293
+ if ('distance' in value || 'sort' in value)
294
+ return false;
295
+ return Array.isArray(value.path);
296
+ }
252
297
  /**
253
298
  * Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
254
299
  * direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
@@ -5,7 +5,7 @@
5
5
  * `import { … } from './query/index.js'` is a drop-in replacement for the
6
6
  * former monolithic `import { … } from './query.js'`.
7
7
  */
8
- export type { AggregateArgs, AggregateResult, ArrayFilter, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByArgs, HavingClause, JsonFilter, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
8
+ export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByArgs, HavingClause, JsonFilter, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
9
9
  export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
10
10
  export { postgresDialect } from '../dialect.js';
11
11
  export type { SqlCacheEntry } from './utils.js';
@@ -18,20 +18,48 @@ export type OrderDirection = 'asc' | 'desc';
18
18
  * Precedence: per-query arg > client `relationLoadStrategy` config > `'join'`.
19
19
  */
20
20
  export type RelationLoadStrategy = 'join' | 'batched';
21
+ /**
22
+ * Reference to ANOTHER COLUMN of the same table inside a where operator,
23
+ * enabling column-to-column comparison:
24
+ *
25
+ * ```ts
26
+ * where: { currentVersionId: { equals: { col: 'publishedVersionId' } } }
27
+ * // → WHERE "current_version_id" = "published_version_id"
28
+ * ```
29
+ *
30
+ * Accepted by `equals`, `not`, `gt`, `gte`, `lt`, and `lte`. The referenced
31
+ * field resolves through the table's columnMap (camelCase accepted, same as a
32
+ * where key) and compiles to a quoted identifier: NO parameter is bound.
33
+ * An unknown referenced field throws {@link ValidationError} (E003).
34
+ *
35
+ * Notes:
36
+ * - `mode: 'insensitive'` cannot be combined with a column reference: it
37
+ * throws E003 (use `client.sql` for `lower(a) = lower(b)`).
38
+ * - On json/jsonb columns `equals` routes to the JSONB containment filter
39
+ * first, so `{ equals: { col } }` there is treated as a JSON value, not a
40
+ * column reference.
41
+ *
42
+ * `F` narrows the referenced name to the table's field names when the
43
+ * surrounding {@link WhereClause} knows the entity type.
44
+ */
45
+ export interface ColumnRef<F extends string = string> {
46
+ col: F;
47
+ }
21
48
  /** Operator object for advanced where filtering */
22
- export interface WhereOperator<V = unknown> {
49
+ export interface WhereOperator<V = unknown, F extends string = string> {
23
50
  /**
24
51
  * Explicit equality: `{ equals: value }` → `column = $n`.
25
52
  * `{ equals: null }` → `column IS NULL`.
53
+ * `{ equals: { col: 'otherField' } }` → `column = "other_field"` ({@link ColumnRef}).
26
54
  * On json/jsonb columns `equals` routes to the JSONB containment filter
27
55
  * ({@link JsonFilter}) instead.
28
56
  */
29
- equals?: V | null;
30
- gt?: V;
31
- gte?: V;
32
- lt?: V;
33
- lte?: V;
34
- not?: V | null;
57
+ equals?: V | ColumnRef<F> | null;
58
+ gt?: V | ColumnRef<F>;
59
+ gte?: V | ColumnRef<F>;
60
+ lt?: V | ColumnRef<F>;
61
+ lte?: V | ColumnRef<F>;
62
+ not?: V | ColumnRef<F> | null;
35
63
  in?: V[];
36
64
  notIn?: V[];
37
65
  contains?: string;
@@ -50,7 +78,7 @@ export interface WhereOperator<V = unknown> {
50
78
  * - A text search filter object ({ search, config? })
51
79
  * - A vector distance filter object ({ distance: { to, metric, lt } }) for pgvector columns
52
80
  */
53
- export type WhereValue<V = unknown> = (V extends Array<infer U> ? TypedRelationFilter<U> : V extends Date ? V : V extends object ? V | TypedToOneFilter<V> | WhereClause<V> : V) | WhereOperator<V> | JsonFilter | ArrayFilter | TextSearchFilter | VectorFilter | null;
81
+ export type WhereValue<V = unknown, F extends string = string> = (V extends Array<infer U> ? TypedRelationFilter<U> : V extends Date ? V : V extends object ? V | TypedToOneFilter<V> | WhereClause<V> : V) | WhereOperator<V, F> | JsonFilter | ArrayFilter | TextSearchFilter | VectorFilter | null;
54
82
  /** Relation filter on a to-many relation property. */
55
83
  export interface TypedRelationFilter<U> {
56
84
  some?: WhereClause<U>;
@@ -71,7 +99,7 @@ export interface TypedToOneFilter<V> {
71
99
  * Relation names can be used with some/every/none sub-filters.
72
100
  */
73
101
  export type WhereClause<T> = {
74
- [K in keyof T]?: WhereValue<T[K]>;
102
+ [K in keyof T]?: WhereValue<T[K], Extract<keyof T, string>>;
75
103
  } & {
76
104
  OR?: WhereClause<T>[];
77
105
  AND?: WhereClause<T>[];
@@ -148,7 +176,7 @@ export type TypedWithClause<R extends object = {}> = [keyof R] extends [never] ?
148
176
  export interface WithOptions<NestedR extends object = {}> {
149
177
  with?: TypedWithClause<NestedR>;
150
178
  where?: Record<string, unknown>;
151
- orderBy?: Record<string, OrderDirection | OrderBySpec>;
179
+ orderBy?: Record<string, OrderDirection | OrderBySpec | JsonPathOrderBy | RelationOrderBy>;
152
180
  limit?: number;
153
181
  /** Only include these fields from the relation */
154
182
  select?: Record<string, boolean>;
@@ -741,6 +769,32 @@ export interface OrderBySpec {
741
769
  sort: OrderDirection;
742
770
  nulls?: 'first' | 'last';
743
771
  }
772
+ /**
773
+ * Ordering by a JSON path on a json/jsonb column of the SAME table:
774
+ *
775
+ * ```ts
776
+ * orderBy: { data: { path: ['weight'], direction: 'asc', type: 'numeric' } }
777
+ * // → ORDER BY ("data" #>> $n::text[])::numeric ASC
778
+ * ```
779
+ *
780
+ * The path is bound as a single text[] parameter (never interpolated).
781
+ * Comparison rule: values extracted from the path compare as TEXT by default;
782
+ * pass `type: 'numeric'` to cast for numeric comparison (`::numeric` on
783
+ * PostgreSQL). The column must be json/jsonb: anything else throws
784
+ * {@link ValidationError} (E003). Non-Postgres engines route through the same
785
+ * dialect JSON-extract hook the JSON where-filters use. Cross-relation
786
+ * (lateral) JSON ordering is NOT supported: same-table columns only.
787
+ */
788
+ export interface JsonPathOrderBy {
789
+ /** JSON path into the column (each element a key or array index). Bound as one text[] param. */
790
+ path: (string | number)[];
791
+ /** Sort direction. Defaults to `'asc'`. */
792
+ direction?: OrderDirection;
793
+ /** Comparison kind for the extracted value. Defaults to `'text'`; `'numeric'` adds a numeric cast. */
794
+ type?: 'numeric' | 'text';
795
+ /** NULLS placement (PostgreSQL / SQLite only: see {@link OrderBySpec}). */
796
+ nulls?: 'first' | 'last';
797
+ }
744
798
  /**
745
799
  * Ordering by a relation, keyed by the relation name in an {@link OrderByClause}:
746
800
  *
@@ -757,9 +811,10 @@ export type RelationOrderBy = {
757
811
  * An orderBy clause maps each key to one of:
758
812
  * - a plain direction (`'asc'` / `'desc'`),
759
813
  * - an {@link OrderBySpec} (`{ sort, nulls }`) for NULLS placement,
814
+ * - for json/jsonb columns, a JSON-path ordering ({@link JsonPathOrderBy}),
760
815
  * - for pgvector columns, a KNN distance ordering ({@link VectorOrderBy}),
761
816
  * - for a relation name, a {@link RelationOrderBy} (`_count` for to-many, a
762
817
  * target column for to-one).
763
818
  */
764
- export type OrderByClause = Record<string, OrderDirection | OrderBySpec | VectorOrderBy | RelationOrderBy>;
819
+ export type OrderByClause = Record<string, OrderDirection | OrderBySpec | JsonPathOrderBy | VectorOrderBy | RelationOrderBy>;
765
820
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "exports": {