turbine-orm 0.31.0 → 0.32.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.
@@ -5,7 +5,7 @@
5
5
  * compiler. Kept out of builder.ts so the class file stays about SQL assembly
6
6
  * and execution rather than filter-shape bookkeeping.
7
7
  */
8
- import type { ArrayFilter, ColumnRef, JsonFilter, JsonPathOrderBy, OrderBySpec, OrderDirection, TextSearchFilter, VectorFilter, VectorOrderBy, WhereOperator } from './types.js';
8
+ import type { ArrayFilter, ColumnRef, JsonFilter, JsonPathOrderBy, OrderBySpec, OrderDirection, RelationPickOrderBy, TextSearchFilter, VectorFilter, VectorOrderBy, WhereOperator } from './types.js';
9
9
  /** Check if a value is a where operator object (has at least one known operator key) */
10
10
  export declare function isWhereOperator(value: unknown): value is WhereOperator;
11
11
  /**
@@ -154,6 +154,18 @@ export declare function isOrderBySpec(value: unknown): value is OrderBySpec;
154
154
  * `distance`/`sort` exclusions keep vector and spec shapes out.
155
155
  */
156
156
  export declare function isJsonPathOrderBy(value: unknown): value is JsonPathOrderBy;
157
+ /**
158
+ * Check if an orderBy value is a pick-row relation ordering:
159
+ * `{ pick: { orderBy, ... }, by, direction?, nulls? }`. The full shape is
160
+ * required — `pick` must be an object carrying `orderBy`, `by` must be
161
+ * present, and no keys outside `{ pick, by, direction, nulls }` — so a to-one
162
+ * relation whose target has real columns literally named `pick` and `by`
163
+ * (whose values are direction strings or `{ sort, nulls }` specs, never an
164
+ * object with `orderBy`) still falls through to column ordering. `distance`
165
+ * (vector), `sort` (OrderBySpec), and a top-level array `path` (JSON-path
166
+ * ordering) are excluded up front.
167
+ */
168
+ export declare function isRelationPickOrderBy(value: unknown): value is RelationPickOrderBy;
157
169
  /**
158
170
  * Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
159
171
  * direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
@@ -294,6 +294,33 @@ export function isJsonPathOrderBy(value) {
294
294
  return false;
295
295
  return Array.isArray(value.path);
296
296
  }
297
+ /**
298
+ * Check if an orderBy value is a pick-row relation ordering:
299
+ * `{ pick: { orderBy, ... }, by, direction?, nulls? }`. The full shape is
300
+ * required — `pick` must be an object carrying `orderBy`, `by` must be
301
+ * present, and no keys outside `{ pick, by, direction, nulls }` — so a to-one
302
+ * relation whose target has real columns literally named `pick` and `by`
303
+ * (whose values are direction strings or `{ sort, nulls }` specs, never an
304
+ * object with `orderBy`) still falls through to column ordering. `distance`
305
+ * (vector), `sort` (OrderBySpec), and a top-level array `path` (JSON-path
306
+ * ordering) are excluded up front.
307
+ */
308
+ export function isRelationPickOrderBy(value) {
309
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
310
+ return false;
311
+ if ('distance' in value || 'sort' in value || Array.isArray(value.path))
312
+ return false;
313
+ const v = value;
314
+ if (!('pick' in v) || !('by' in v))
315
+ return false;
316
+ if (typeof v.pick !== 'object' || v.pick === null || Array.isArray(v.pick) || !('orderBy' in v.pick))
317
+ return false;
318
+ for (const key of Object.keys(v)) {
319
+ if (key !== 'pick' && key !== 'by' && key !== 'direction' && key !== 'nulls')
320
+ return false;
321
+ }
322
+ return true;
323
+ }
297
324
  /**
298
325
  * Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
299
326
  * 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, 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';
8
+ export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, HavingClause, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationPickBy, RelationPickOrderBy, 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';
@@ -596,20 +596,87 @@ export type HavingClause<T> = {
596
596
  } & {
597
597
  [K in keyof T & string]?: HavingAggregateFilter;
598
598
  };
599
+ /**
600
+ * A JSON-path group key in {@link GroupByArgs.by}: groups by the value
601
+ * extracted at `path` from a json/jsonb column. Emits
602
+ * `(col #>> $n::text[]) AS "alias"` in SELECT and the same expression in
603
+ * GROUP BY (the path is bound as one text[] param, never interpolated).
604
+ * Result rows key by `alias` (default: the last path segment; a collision
605
+ * with another result key throws {@link ValidationError} E003).
606
+ */
607
+ export interface JsonPathGroupKey {
608
+ /** json/jsonb column (camelCase field name, columnMap-resolved). */
609
+ field: string;
610
+ /** JSON path into the column (each element a key or array index). Bound as one text[] param. */
611
+ path: (string | number)[];
612
+ /** Result key for the group value. Defaults to the last path segment. */
613
+ alias?: string;
614
+ }
615
+ /**
616
+ * A JSON-path aggregate target inside `_sum` / `_avg` / `_min` / `_max` of
617
+ * {@link GroupByArgs}: aggregates the value extracted at `path` from a
618
+ * json/jsonb column, e.g. `SUM((col #>> $n::text[])::numeric)`. The arg key
619
+ * is the result alias. `_sum`/`_avg` always cast numeric (a text sum is
620
+ * meaningless); `_min`/`_max` compare as text unless `type: 'numeric'`.
621
+ */
622
+ export interface JsonPathAggregateTarget {
623
+ /** json/jsonb column (camelCase field name, columnMap-resolved). */
624
+ field: string;
625
+ /** JSON path into the column (each element a key or array index). Bound as one text[] param. */
626
+ path: (string | number)[];
627
+ /** Comparison/aggregation kind. `_sum`/`_avg` are always numeric; `_min`/`_max` default to text. */
628
+ type?: 'numeric' | 'text';
629
+ }
630
+ /**
631
+ * Per-aggregate spec map for `_sum` / `_avg` / `_min` / `_max` in
632
+ * {@link GroupByArgs}: `true` keeps the existing plain-column behavior (the
633
+ * key is a column field name); a {@link JsonPathAggregateTarget} aggregates a
634
+ * JSON path (the key doubles as the result alias).
635
+ */
636
+ export type GroupByAggregateSpec<T> = Partial<Record<keyof T & string, boolean>> | Record<string, boolean | JsonPathAggregateTarget>;
637
+ /**
638
+ * DISTINCT ON row source for {@link GroupByArgs} (PostgreSQL only: other
639
+ * engines throw {@link UnsupportedFeatureError} E017): the groupBy runs over
640
+ * one representative row per `columns` combination instead of the raw table.
641
+ *
642
+ * ```sql
643
+ * FROM (
644
+ * SELECT DISTINCT ON ("instance_id") * FROM "versions"
645
+ * WHERE <args.where> ORDER BY "instance_id", "created_at" DESC
646
+ * ) AS "versions"
647
+ * ```
648
+ *
649
+ * `orderBy` is REQUIRED (it decides which row survives per combination:
650
+ * without it the picked row would be nondeterministic); the wrapper ORDER BY
651
+ * is `columns` first, then this orderBy. `args.where` applies INSIDE the
652
+ * wrapper (filter before picking).
653
+ */
654
+ export interface GroupByDistinctOn<T> {
655
+ /** DISTINCT ON columns: one surviving row per combination. */
656
+ columns: (keyof T & string)[];
657
+ /** Which row survives per combination (required for determinism). */
658
+ orderBy: Record<string, OrderDirection | OrderBySpec | JsonPathOrderBy>;
659
+ }
599
660
  export interface GroupByArgs<T> {
600
- by: (keyof T & string)[];
661
+ /** Group keys: plain column field names and/or JSON-path keys ({@link JsonPathGroupKey}). */
662
+ by: ((keyof T & string) | JsonPathGroupKey)[];
601
663
  where?: WhereClause<T>;
664
+ /**
665
+ * PostgreSQL only: group over one representative row per column combination
666
+ * (`SELECT DISTINCT ON … ORDER BY …` row source). See {@link GroupByDistinctOn}.
667
+ */
668
+ distinctOn?: GroupByDistinctOn<T>;
602
669
  /** Include count of each group */
603
670
  _count?: true;
604
- /** Sum of numeric fields in each group */
605
- _sum?: Partial<Record<keyof T & string, boolean>>;
606
- /** Average of numeric fields in each group */
607
- _avg?: Partial<Record<keyof T & string, boolean>>;
608
- /** Minimum value of fields in each group */
609
- _min?: Partial<Record<keyof T & string, boolean>>;
610
- /** Maximum value of fields in each group */
611
- _max?: Partial<Record<keyof T & string, boolean>>;
612
- /** Filter whole groups by their aggregate values (SQL HAVING). */
671
+ /** Sum of numeric fields (or JSON paths: see {@link JsonPathAggregateTarget}) in each group */
672
+ _sum?: GroupByAggregateSpec<T>;
673
+ /** Average of numeric fields (or JSON paths) in each group */
674
+ _avg?: GroupByAggregateSpec<T>;
675
+ /** Minimum value of fields (or JSON paths) in each group */
676
+ _min?: GroupByAggregateSpec<T>;
677
+ /** Maximum value of fields (or JSON paths) in each group */
678
+ _max?: GroupByAggregateSpec<T>;
679
+ /** Filter whole groups by their aggregate values (SQL HAVING). JSON-path aggregates key by their alias. */
613
680
  having?: HavingClause<T>;
614
681
  /** Order groups (supports {@link OrderBySpec} for NULLS placement). */
615
682
  orderBy?: Record<string, OrderDirection | OrderBySpec>;
@@ -807,6 +874,74 @@ export interface JsonPathOrderBy {
807
874
  export type RelationOrderBy = {
808
875
  _count: OrderDirection;
809
876
  } | Record<string, OrderDirection | OrderBySpec>;
877
+ /**
878
+ * The ordering value extracted from the picked row in a
879
+ * {@link RelationPickOrderBy}: either a plain target column name (camelCase,
880
+ * columnMap-resolved) or a JSON path into a json/jsonb target column.
881
+ * Values extracted from a JSON path compare as TEXT by default; pass
882
+ * `type: 'numeric'` to add a numeric cast.
883
+ */
884
+ export type RelationPickBy = string | {
885
+ /** json/jsonb column on the relation target. */
886
+ field: string;
887
+ /**
888
+ * JSON path into the column (each element a key or array index). Bound
889
+ * as ONE param: a text[] on PostgreSQL, a `'$'`-rooted JSONPath string
890
+ * on engines whose JSON functions take one (SQLite/MySQL/SQL Server).
891
+ */
892
+ path: (string | number)[];
893
+ /** Comparison kind for the extracted value. Defaults to `'text'`. */
894
+ type?: 'numeric' | 'text';
895
+ };
896
+ /**
897
+ * Pick-row relation ordering: order a parent query by a value read from ONE
898
+ * related row of a hasMany relation, keyed by the relation name in an
899
+ * {@link OrderByClause}. Compiles to a correlated scalar subquery in ORDER BY:
900
+ *
901
+ * ```ts
902
+ * orderBy: {
903
+ * versions: {
904
+ * pick: { orderBy: { createdAt: 'desc' } }, // which related row
905
+ * by: { field: 'data', path: ['title'] }, // value to sort the parents by
906
+ * direction: 'asc',
907
+ * nulls: 'last',
908
+ * },
909
+ * }
910
+ * // → ORDER BY (SELECT ord0."data" #>> $n::text[] FROM "versions" ord0
911
+ * // WHERE ord0."instance_id" = "instances"."id"
912
+ * // ORDER BY ord0."created_at" DESC LIMIT 1) ASC NULLS LAST
913
+ * ```
914
+ *
915
+ * `pick.orderBy` is REQUIRED (it makes the picked row deterministic) and
916
+ * supports the same surface as a relation `with` orderBy on the target (plain
917
+ * columns, {@link OrderBySpec} nulls, {@link JsonPathOrderBy}). `pick.where`
918
+ * optionally filters the candidate rows before picking. hasMany relations
919
+ * only; top-level findMany orderBy only (manyToMany, to-one relations, and
920
+ * nested `with` orderBy throw {@link ValidationError} E003).
921
+ */
922
+ export interface RelationPickOrderBy {
923
+ /** Which related row supplies the value. */
924
+ pick: {
925
+ /** Inner ORDER BY choosing the row (required for determinism). */
926
+ orderBy: Record<string, OrderDirection | OrderBySpec | JsonPathOrderBy>;
927
+ /** Optional filter on the candidate rows before picking. */
928
+ where?: Record<string, unknown>;
929
+ };
930
+ /** The value on the picked row to order the parents by. */
931
+ by: RelationPickBy;
932
+ /** Sort direction for the parents. Defaults to `'asc'`. */
933
+ direction?: OrderDirection;
934
+ /**
935
+ * NULLS placement (PostgreSQL / SQLite only: see {@link OrderBySpec}).
936
+ *
937
+ * A parent with NO related rows sorts by NULL. When `nulls` is not set,
938
+ * pick ordering defaults to `NULLS LAST` in BOTH directions, so parents
939
+ * with zero related rows always come last (Postgres's own DESC default is
940
+ * NULLS FIRST, which would put every childless parent at the top of a
941
+ * "highest first" sort). Set `nulls` explicitly to override.
942
+ */
943
+ nulls?: 'first' | 'last';
944
+ }
810
945
  /**
811
946
  * An orderBy clause maps each key to one of:
812
947
  * - a plain direction (`'asc'` / `'desc'`),
@@ -814,7 +949,8 @@ export type RelationOrderBy = {
814
949
  * - for json/jsonb columns, a JSON-path ordering ({@link JsonPathOrderBy}),
815
950
  * - for pgvector columns, a KNN distance ordering ({@link VectorOrderBy}),
816
951
  * - for a relation name, a {@link RelationOrderBy} (`_count` for to-many, a
817
- * target column for to-one).
952
+ * target column for to-one) or a pick-row ordering
953
+ * ({@link RelationPickOrderBy}, hasMany only).
818
954
  */
819
- export type OrderByClause = Record<string, OrderDirection | OrderBySpec | JsonPathOrderBy | VectorOrderBy | RelationOrderBy>;
955
+ export type OrderByClause = Record<string, OrderDirection | OrderBySpec | JsonPathOrderBy | VectorOrderBy | RelationOrderBy | RelationPickOrderBy>;
820
956
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.31.0",
3
+ "version": "0.32.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": {