kysely-hydrate 0.3.0 → 0.4.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 CHANGED
@@ -131,6 +131,7 @@ By design, Kysely has the following constraints:
131
131
  - [`.hasMany()`](#hasmany)
132
132
  - [`.hasOne()`](#hasone)
133
133
  - [`.hasOneOrThrow()`](#hasoneorthrow)
134
+ - [Hydrated lateral joins](#hydrated-lateral-joins)
134
135
  - [Application-level joins with `.attach*()`](#application-level-joins-with-attach)
135
136
  - [`.attachMany()`](#attachmany)
136
137
  - [`.attachOne()`](#attachone)
@@ -465,12 +466,86 @@ but you know the record must exist.
465
466
 
466
467
  > [!NOTE]
467
468
  > Like `hydrate()`, `hasOneOrThrow()` also accepts an optional final `keyBy` argument with the
468
- > same semantics (see [Keying and deduplication with `keyBy`](#keying-and-deduplication-with-keyby)).
469
+ > same semantics (see [Keying and deduplication with
470
+ > `keyBy`](#keying-and-deduplication-with-keyby)).
471
+
472
+ #### Hydrated lateral joins
473
+
474
+ Kysely supports lateral joins by passing a subquery to methods like
475
+ `innerJoinLateral`, `leftJoinLateral`, etc.
476
+
477
+ In addition to Kysely's default behavior, Kysely Hydrate also lets you pass an
478
+ **aliased hydrated subquery** to these methods. When you do, the subquery’s
479
+ selections are automatically added to the parent query (with the usual
480
+ prefixing), and the nested result is hydrated according to the nested hydration
481
+ config. This makes it possible to compose hydrated queries through lateral
482
+ joins.
483
+
484
+ Here’s an example selecting each user and their latest post (scoped per-user
485
+ inside the lateral subquery):
486
+
487
+ ```ts
488
+ const query = hydrate(db.selectFrom("users").select(["users.id", "users.username"]), "id")
489
+ .hasOne("latestPost", ({ innerJoinLateral }) =>
490
+ innerJoinLateral((db) =>
491
+ hydrate(
492
+ db
493
+ .selectFrom("posts")
494
+ .select(["posts.id", "posts.title"])
495
+ .whereRef("posts.user_id", "=", "users.id")
496
+ .orderBy("posts.id", "desc")
497
+ .limit(1),
498
+ "id",
499
+ ).as("latest_post_subquery"),
500
+ // You can omit `(join) => join.onTrue()` for the hydrated-subquery overload.
501
+ ),
502
+ );
503
+
504
+ const result = await query.execute();
505
+ // ⬇
506
+ type Result = Array<{
507
+ id: number;
508
+ username: string;
509
+ latestPost: { id: number; title: string };
510
+ }>;
511
+ ```
512
+
513
+ The hydrated lateral subquery is “hoisted” into the parent select list, and its
514
+ columns are prefixed under the relation key (e.g. `latestPost$$id`,
515
+ `latestPost$$title`) so the hydration layer can un-flatten them.
516
+
517
+ ```sql
518
+ select
519
+ "users"."id",
520
+ "users"."username",
521
+ "latest_post_subquery"."id" as "latestPost$$id",
522
+ "latest_post_subquery"."title" as "latestPost$$title"
523
+ from "users"
524
+ inner join lateral (
525
+ select "posts"."id", "posts"."title"
526
+ from "posts"
527
+ where "posts"."user_id" = "users"."id"
528
+ limit ?
529
+ ) as "latest_post_subquery" on true
530
+ ```
531
+
532
+ > [!WARNING]
533
+ > You cannot use `.selectAll()` in cases where Kysely Hydrate needs to resolve
534
+ > and rewrite nested selections for prefixing. Prefer explicit `.select([...])`
535
+ > (it will throw if selections can’t be resolved).
536
+
537
+ > [!WARNING]
538
+ > You're still writing SQL. Lateral joins are powerful, but it’s easy to build
539
+ > queries that don’t compose the way you expect (limits/order-by scopes,
540
+ > correlation, etc). When in doubt, inspect the output with
541
+ > `.toQuery().compile()`.
469
542
 
470
543
  #### SQL output
471
544
 
472
545
  Kysely Hydrate produces the SQL you tell it to with exactly one exception: it
473
- aliases nested selections to avoid naming collisions.
546
+ aliases nested selections to avoid naming collisions. (Kysely Hydrate also
547
+ hoists selections from hydrated lateral join subqueries, as described
548
+ [above](#hydrated-lateral-joins)—but only if you use that feature.)
474
549
 
475
550
  For example, `users.id` remains `users.id`, but `posts.id` nested under "posts"
476
551
  becomes `posts$$id`. The hydration layer then un-flattens these aliases back
package/dist/index.d.mts CHANGED
@@ -212,9 +212,10 @@ interface FullHydrator<Input, Output> extends MappedHydrator<Input, Output> {
212
212
  * Configures which fields to include in the hydrated output.
213
213
  *
214
214
  * @param fields - An object mapping field names to either `true` (include as-is)
215
- * or a transformation function
215
+ * or a transformation function. Also accepts an array of field names to include.
216
216
  * @returns A new Hydrator with the fields configuration merged
217
217
  */
218
+ fields<F extends readonly (keyof Input)[]>(fields: F): FullHydrator<Input, Extend<Output, Pick<Input, F[number]>>>;
218
219
  fields<F extends Fields<Input>>(fields: F): FullHydrator<Input, Extend<Output, InferFields<Input, F>>>;
219
220
  /**
220
221
  * Omits specified fields from the hydrated output.
@@ -388,10 +389,10 @@ interface MappedHydratedQueryBuilder<Prefix extends string, QueryDB, QueryTB ext
388
389
  QueryDB: QueryDB;
389
390
  QueryTB: QueryTB;
390
391
  QueryRow: QueryRow;
392
+ LocalDB: LocalDB;
391
393
  LocalRow: LocalRow;
392
394
  HydratedRow: HydratedRow;
393
395
  IsNullable: IsNullable;
394
- NestedDB: LocalDB;
395
396
  HasJoin: HasJoin;
396
397
  } | undefined;
397
398
  /**
@@ -447,6 +448,24 @@ interface MappedHydratedQueryBuilder<Prefix extends string, QueryDB, QueryTB ext
447
448
  * @returns A MappedHydratedQueryBuilder with the transformation added
448
449
  */
449
450
  map<NewHydratedRow>(transform: (row: k$1.Simplify<HydratedRow>) => NewHydratedRow): MappedHydratedQueryBuilder<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, NewHydratedRow, IsNullable, HasJoin>;
451
+ /**
452
+ * Creates an aliased version of this query builder, suitable for use in join
453
+ * subqueries.
454
+ *
455
+ * **Example:**
456
+ * ```ts
457
+ * .hasMany('pets', ({ innerJoinLateral }) =>
458
+ * innerJoinLateral(
459
+ * (eb) => hydrate(eb.selectFrom('pet').select(['id', 'name'])).as('p'),
460
+ * (join) => join.onTrue()
461
+ * )
462
+ * )
463
+ * ```
464
+ *
465
+ * @param alias - The alias for this subquery
466
+ * @returns An AliasedHydratedExpression that can be used in lateral joins
467
+ */
468
+ as<Alias extends string>(alias: Alias): AliasedHydratedQueryBuilder<QueryRow, HydratedRow, Alias>;
450
469
  }
451
470
  /**
452
471
  * A query builder that supports both mapping and nested joins.
@@ -771,20 +790,49 @@ interface HydratedQueryBuilder<Prefix extends string, QueryDB, QueryTB extends k
771
790
  /**
772
791
  * Just like {@link innerJoin} but adds an `inner join lateral` instead of an
773
792
  * `inner join`.
793
+ *
794
+ * In addition to matching Kysely's allowed arguments, you may also pass a
795
+ * `HydratedQueryBuilder` as your subquery. If you do, anything it selects
796
+ * will automatically be added to the parent query's selections (with
797
+ * prefixing). This allows you to compose hydrated queries.
798
+ *
799
+ * @example
800
+ * ```ts
801
+ * const query = hydrate(db.selectFrom("users").select(["users.id", "users.username"]))
802
+ * .hasMany("posts", ({ innerJoinLateral }) =>
803
+ * innerJoinLateral(hydrate(db.selectFrom("posts").select(["posts.id", "posts.title"])))
804
+ * );
805
+ *
806
+ * const result = await query.execute();
807
+ *
808
+ * // result is an array of objects with the following shape:
809
+ * {
810
+ * id: number;
811
+ * username: string;
812
+ * posts: Array<{ id: number; title: string }>;
813
+ * }
814
+ * ```
815
+ *
816
+ * For the hydrated subquery case, you may omit the second "join builder" argument.
817
+ * By default, it will be `(join) => join.onTrue()`, which is typically what you want
818
+ * for a lateral join.
774
819
  */
775
820
  innerJoinLateral<TE extends k$1.TableExpression<QueryDB, QueryTB>, K1 extends k$1.JoinReferenceExpression<QueryDB, QueryTB, TE>, K2 extends k$1.JoinReferenceExpression<QueryDB, QueryTB, TE>>(table: TE, k1: K1, k2: K2): HydratedQueryBuilderWithInnerJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, TE>;
776
- innerJoinLateral<TE extends k$1.TableExpression<QueryDB, QueryTB>, FN extends k$1.JoinCallbackExpression<QueryDB, QueryTB, TE>>(table: TE, callback: FN): HydratedQueryBuilderWithInnerJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, TE>;
821
+ innerJoinLateral<TE extends k$1.TableExpression<QueryDB, QueryTB>, FN extends k$1.JoinCallbackExpression<QueryDB, QueryTB, NoInfer<TE>>>(table: TE, callback: FN): HydratedQueryBuilderWithInnerJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, TE>;
822
+ innerJoinLateral<Alias extends string, NestedQueryRow, NestedHydratedRow, FN extends HydratedJoinCallbackExpression<QueryDB, QueryTB, Alias, NoInfer<NestedQueryRow>>>(hydratedTable: AliasedHydratedQueryBuilderOrFactory<QueryDB, QueryTB, Alias, NestedQueryRow, NestedHydratedRow>, callback?: FN): HydratedQueryBuilderWithHydratedInnerJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, Alias, NestedQueryRow, NestedHydratedRow>;
777
823
  /**
778
- * Just like {@link leftJoin} but adds a `left join lateral` instead of a
779
- * `left join`.
824
+ * Just like {@link innerJoinLateral} but adds a `left join lateral` instead of a
825
+ * `inner join lateral`.
780
826
  */
781
827
  leftJoinLateral<TE extends k$1.TableExpression<QueryDB, QueryTB>, K1 extends k$1.JoinReferenceExpression<QueryDB, QueryTB, TE>, K2 extends k$1.JoinReferenceExpression<QueryDB, QueryTB, TE>>(table: TE, k1: K1, k2: K2): HydratedQueryBuilderWithLeftJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, HasJoin, TE>;
782
- leftJoinLateral<TE extends k$1.TableExpression<QueryDB, QueryTB>, FN extends k$1.JoinCallbackExpression<QueryDB, QueryTB, TE>>(table: TE, callback: FN): HydratedQueryBuilderWithLeftJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, HasJoin, TE>;
828
+ leftJoinLateral<TE extends k$1.TableExpression<QueryDB, QueryTB>, FN extends k$1.JoinCallbackExpression<QueryDB, QueryTB, NoInfer<TE>>>(table: TE, callback: FN): HydratedQueryBuilderWithLeftJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, HasJoin, TE>;
829
+ leftJoinLateral<Alias extends string, NestedQueryRow, NestedHydratedRow, FN extends HydratedJoinCallbackExpression<QueryDB, QueryTB, Alias, NoInfer<NestedQueryRow>>>(hydratedTable: AliasedHydratedQueryBuilderOrFactory<QueryDB, QueryTB, Alias, NestedQueryRow, NestedHydratedRow>, callback?: FN): HydratedQueryBuilderWithHydratedLeftJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, HasJoin, Alias, NestedQueryRow, NestedHydratedRow>;
783
830
  /**
784
- * Just like {@link innerJoin} but adds a `cross join lateral` instead of an
785
- * `inner join`.
831
+ * Just like {@link innerJoinLateral} but adds a `cross join lateral` instead of an
832
+ * `inner join lateral`.
786
833
  */
787
834
  crossJoinLateral<TE extends k$1.TableExpression<QueryDB, QueryTB>>(table: TE): HydratedQueryBuilderWithInnerJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, TE>;
835
+ crossJoinLateral<Alias extends string, NestedQueryRow, NestedHydratedRow>(hydratedTable: AliasedHydratedQueryBuilderOrFactory<QueryDB, QueryTB, Alias, NestedQueryRow, NestedHydratedRow>): HydratedQueryBuilderWithHydratedInnerJoin<Prefix, QueryDB, QueryTB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable, Alias, NestedQueryRow, NestedHydratedRow>;
788
836
  /**
789
837
  * Adds a select statement to the query.
790
838
  *
@@ -802,6 +850,36 @@ type HydratedQueryBuilderWithInnerJoin<Prefix extends string, QueryDB, QueryTB e
802
850
  type HydratedQueryBuilderWithLeftJoin<Prefix extends string, QueryDB, QueryTB extends keyof QueryDB, QueryRow, _LocalDB, LocalRow, HydratedRow, _IsNullable extends boolean, AlreadyHadJoin extends boolean, TE extends k$1.TableExpression<QueryDB, QueryTB>> = k$1.SelectQueryBuilderWithLeftJoin<QueryDB, QueryTB, QueryRow, TE> extends k$1.SelectQueryBuilder<infer JoinedDB, infer JoinedTB, infer JoinedRow> ? HydratedQueryBuilder<Prefix, JoinedDB, JoinedTB, JoinedRow, AlreadyHadJoin extends true ? JoinedDB : InferDB<k$1.SelectQueryBuilderWithInnerJoin<QueryDB, QueryTB, QueryRow, TE>>, LocalRow, HydratedRow, true,
803
851
  // Left joins always produce nullable rows.
804
852
  true> : never;
853
+ type HydratedJoinCallbackExpression<QueryDB, QueryTB extends keyof QueryDB, Alias extends string, NestedQueryRow> = k$1.JoinCallbackExpression<QueryDB, QueryTB, AliasedHydratedQueryBuilderTableExpression<NestedQueryRow, Alias>>;
854
+ type HydratedQueryBuilderWithHydratedInnerJoin<Prefix extends string, QueryDB, QueryTB extends keyof QueryDB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable extends boolean, Alias extends string, NestedQueryRow, NestedHydratedRow> = HydratedQueryBuilderWithInnerJoin<Prefix, QueryDB, QueryTB, Extend<QueryRow, ApplyPrefixes<Prefix, NestedQueryRow>>, LocalDB, Extend<LocalRow, NestedQueryRow>, Extend<HydratedRow, NestedHydratedRow>, IsNullable, AliasedHydratedQueryBuilderTableExpression<NestedQueryRow, Alias>>;
855
+ type HydratedQueryBuilderWithHydratedLeftJoin<Prefix extends string, QueryDB, QueryTB extends keyof QueryDB, QueryRow, LocalDB, LocalRow, HydratedRow, IsNullable extends boolean, AlreadyHadJoin extends boolean, Alias extends string, NestedQueryRow, NestedHydratedRow> = HydratedQueryBuilderWithLeftJoin<Prefix, QueryDB, QueryTB, Extend<QueryRow, ApplyPrefixes<Prefix, NestedQueryRow>>, LocalDB, Extend<LocalRow, NestedQueryRow>, Extend<HydratedRow, NestedHydratedRow>, IsNullable, AlreadyHadJoin, AliasedHydratedQueryBuilderTableExpression<NestedQueryRow, Alias>>;
856
+ type AliasedHydratedQueryBuilderOrFactory<QueryDB, QueryTB extends keyof QueryDB, Alias extends string, NestedQueryRow, NestedHydratedRow> = AliasedHydratedQueryBuilder<NestedQueryRow, NestedHydratedRow, Alias> | AliasedHydratedQueryBuilderFactory<QueryDB, QueryTB, Alias, NestedQueryRow, NestedHydratedRow>;
857
+ type AliasedHydratedQueryBuilderFactory<QueryDB, QueryTB extends keyof QueryDB, Alias extends string, NestedQueryRow, NestedHydratedRow> = (eb: k$1.ExpressionBuilder<QueryDB, QueryTB>) => AliasedHydratedQueryBuilder<NestedQueryRow, NestedHydratedRow, Alias>;
858
+ type AliasedHydratedQueryBuilderTableExpression<QueryRow, Alias extends string> = k$1.AliasedSelectQueryBuilder<QueryRow, Alias>;
859
+ type HqbForAlias<QueryRow, HydratedRow> = MappedHydratedQueryBuilder<any, any, any, QueryRow, any, any, HydratedRow, any, any>;
860
+ /**
861
+ * Represents a {@link HydratedQueryBuilder} that has been aliased with `.as()`.
862
+ * Analogous to {@link k.AliasedSelectQueryBuilder}.
863
+ */
864
+ declare class AliasedHydratedQueryBuilder<QueryRow, HydratedRow, Alias extends string> {
865
+ #private;
866
+ readonly isAliasedHydratedQueryBuilder: true;
867
+ constructor(alias: Alias, hydratedQueryBuilder: HqbForAlias<QueryRow, HydratedRow>);
868
+ /**
869
+ * The alias!
870
+ */
871
+ get alias(): Alias;
872
+ /**
873
+ * The underlying {@link HydratedQueryBuilder}, which was aliased with `.as()`.
874
+ */
875
+ get hydratedQueryBuilder(): HqbForAlias<QueryRow, HydratedRow>;
876
+ /**
877
+ * Produces an {@link k.AliasedSelectQueryBuilder} for the
878
+ * {@link k.SelectQueryBuilder} underlying this {@link HydratedQueryBuilder}.
879
+ *
880
+ */
881
+ toAliasedQuery(): AliasedHydratedQueryBuilderTableExpression<QueryRow, Alias>;
882
+ }
805
883
  /**
806
884
  * Creates a new {@link HydratedQueryBuilder} from a Kysely select query.
807
885
  * This enables nested joins and automatic hydration of flat SQL results into nested objects.
package/dist/index.mjs CHANGED
@@ -275,7 +275,7 @@ var HydratorImpl = class HydratorImpl {
275
275
  fields(fields) {
276
276
  return new HydratorImpl({
277
277
  ...this.#props,
278
- fields: addObjectToMap(this.#props.fields, fields)
278
+ fields: Array.isArray(fields) ? fields.reduce((map$1, field) => map$1.set(field, true), new Map(this.#props.fields)) : addObjectToMap(this.#props.fields, fields)
279
279
  });
280
280
  }
281
281
  omit(keys) {
@@ -526,11 +526,27 @@ const fakeQb = k.createSelectQueryBuilder({
526
526
  function prefixSelectArg(prefix, selection) {
527
527
  return fakeQb.select(selection).toOperationNode().selections?.map((selectionNode) => prefixSelectionNode(selectionNode, prefix)) ?? [];
528
528
  }
529
+ /**
530
+ * Produces selections for a parent query to select everything selected in a
531
+ * subquery, but aliased with the given prefix.
532
+ */
533
+ function hoistAndPrefixSelections(prefix, qb) {
534
+ const table = qb.alias;
535
+ if (typeof table !== "string") throw new UnexpectedComplexAliasError();
536
+ const node = qb.expression.toOperationNode();
537
+ if (!node.selections) return [];
538
+ const eb = k.expressionBuilder();
539
+ return node.selections.map((selectionNode) => {
540
+ const name = extractSelectionName(selectionNode);
541
+ return new PrefixedAliasedExpression(eb.ref(`${table}.${name}`), prefix, name);
542
+ });
543
+ }
529
544
  var PrefixedAliasedExpression = class extends k.AliasedExpressionWrapper {
530
545
  originalName;
531
546
  constructor(node, prefix, originalName) {
532
547
  const alias = applyPrefix(prefix, originalName);
533
- super(new k.ExpressionWrapper(node), alias);
548
+ const expression = k.isExpression(node) ? node : new k.ExpressionWrapper(node);
549
+ super(expression, alias);
534
550
  this.originalName = originalName;
535
551
  }
536
552
  };
@@ -558,6 +574,39 @@ function extractSelectionName(selectionNode) {
558
574
  //#endregion
559
575
  //#region src/query-builder.ts
560
576
  /**
577
+ * Represents a {@link HydratedQueryBuilder} that has been aliased with `.as()`.
578
+ * Analogous to {@link k.AliasedSelectQueryBuilder}.
579
+ */
580
+ var AliasedHydratedQueryBuilder = class {
581
+ isAliasedHydratedQueryBuilder = true;
582
+ #alias;
583
+ #hydratedQueryBuilder;
584
+ constructor(alias, hydratedQueryBuilder) {
585
+ this.#alias = alias;
586
+ this.#hydratedQueryBuilder = hydratedQueryBuilder;
587
+ }
588
+ /**
589
+ * The alias!
590
+ */
591
+ get alias() {
592
+ return this.#alias;
593
+ }
594
+ /**
595
+ * The underlying {@link HydratedQueryBuilder}, which was aliased with `.as()`.
596
+ */
597
+ get hydratedQueryBuilder() {
598
+ return this.#hydratedQueryBuilder;
599
+ }
600
+ /**
601
+ * Produces an {@link k.AliasedSelectQueryBuilder} for the
602
+ * {@link k.SelectQueryBuilder} underlying this {@link HydratedQueryBuilder}.
603
+ *
604
+ */
605
+ toAliasedQuery() {
606
+ return this.#hydratedQueryBuilder.toQuery().as(this.#alias);
607
+ }
608
+ };
609
+ /**
561
610
  * Implementation of the {@link HydratedQueryBuilder} interface as well as the
562
611
  * {@link MappedHydratedQueryBuilder} interface; there is no runtime distinction.
563
612
  */
@@ -594,6 +643,9 @@ var HydratedQueryBuilderImpl = class HydratedQueryBuilderImpl {
594
643
  hydrator: this.#props.hydrator.map(transform)
595
644
  });
596
645
  }
646
+ as(alias) {
647
+ return new AliasedHydratedQueryBuilder(alias, this);
648
+ }
597
649
  #hydrate(rows) {
598
650
  const isArray = Array.isArray(rows);
599
651
  const firstRow = isArray ? rows[0] : rows;
@@ -637,7 +689,7 @@ var HydratedQueryBuilderImpl = class HydratedQueryBuilderImpl {
637
689
  hydrator: asFullHydrator(this.#props.hydrator).extend(hydrator)
638
690
  });
639
691
  }
640
- #addJoin(mode, key, jb, keyBy = DEFAULT_KEY_BY) {
692
+ #has(mode, key, jb, keyBy = DEFAULT_KEY_BY) {
641
693
  const outputNb = jb(new HydratedQueryBuilderImpl({
642
694
  qb: this.#props.qb,
643
695
  prefix: makePrefix(this.#props.prefix, key),
@@ -650,13 +702,13 @@ var HydratedQueryBuilderImpl = class HydratedQueryBuilderImpl {
650
702
  });
651
703
  }
652
704
  hasMany(key, jb, keyBy) {
653
- return this.#addJoin("many", key, jb, keyBy);
705
+ return this.#has("many", key, jb, keyBy);
654
706
  }
655
707
  hasOne(key, jb, keyBy) {
656
- return this.#addJoin("one", key, jb, keyBy);
708
+ return this.#has("one", key, jb, keyBy);
657
709
  }
658
710
  hasOneOrThrow(key, jb, keyBy) {
659
- return this.#addJoin("oneOrThrow", key, jb, keyBy);
711
+ return this.#has("oneOrThrow", key, jb, keyBy);
660
712
  }
661
713
  #addAttach(mode, key, fetchFn, keys) {
662
714
  return new HydratedQueryBuilderImpl({
@@ -678,26 +730,49 @@ var HydratedQueryBuilderImpl = class HydratedQueryBuilderImpl {
678
730
  return new HydratedQueryBuilderImpl({
679
731
  ...this.#props,
680
732
  qb: this.#props.qb.select(prefixedSelections),
681
- hydrator: asFullHydrator(this.#props.hydrator).fields(Object.fromEntries(prefixedSelections.map((selection$1) => [selection$1.originalName, true])))
733
+ hydrator: asFullHydrator(this.#props.hydrator).fields(prefixedSelections.map((selection$1) => selection$1.originalName))
734
+ });
735
+ }
736
+ #join(method, from, ...args) {
737
+ if (typeof from === "function") {
738
+ const result = from(k.expressionBuilder());
739
+ if (result instanceof AliasedHydratedQueryBuilder) return this.#joinHydrated(method, result, ...args);
740
+ } else if (from instanceof AliasedHydratedQueryBuilder) return this.#joinHydrated(method, from, ...args);
741
+ return this.modify((qb) => qb[method](from, ...args));
742
+ }
743
+ #joinHydrated(method, from, ...args) {
744
+ if (!args.length && (method === "leftJoinLateral" || method === "innerJoinLateral")) args = [(join) => join.onTrue()];
745
+ const aliasedQb = from.toAliasedQuery();
746
+ const hoistedSelections = hoistAndPrefixSelections(this.#props.prefix, aliasedQb);
747
+ const nestedHydrator = from.hydratedQueryBuilder.#props.hydrator;
748
+ let newQb = this.#props.qb;
749
+ newQb = newQb[method](aliasedQb, ...args);
750
+ newQb = newQb.select(hoistedSelections);
751
+ let newHydrator = asFullHydrator(this.#props.hydrator).extend(nestedHydrator);
752
+ newHydrator = ensureFields(newHydrator, hoistedSelections.map((selection) => selection.originalName).filter((name) => !hasAnyPrefix(name)));
753
+ return new HydratedQueryBuilderImpl({
754
+ ...this.#props,
755
+ qb: newQb,
756
+ hydrator: newHydrator
682
757
  });
683
758
  }
684
759
  innerJoin(...args) {
685
- return this.modify((qb) => qb.innerJoin(...args));
760
+ return this.#join("innerJoin", ...args);
686
761
  }
687
762
  leftJoin(...args) {
688
- return this.modify((qb) => qb.leftJoin(...args));
763
+ return this.#join("leftJoin", ...args);
689
764
  }
690
765
  crossJoin(...args) {
691
- return this.modify((qb) => qb.crossJoin(...args));
766
+ return this.#join("crossJoin", ...args);
692
767
  }
693
768
  innerJoinLateral(...args) {
694
- return this.modify((qb) => qb.innerJoinLateral(...args));
769
+ return this.#join("innerJoinLateral", ...args);
695
770
  }
696
771
  leftJoinLateral(...args) {
697
- return this.modify((qb) => qb.leftJoinLateral(...args));
772
+ return this.#join("leftJoinLateral", ...args);
698
773
  }
699
774
  crossJoinLateral(...args) {
700
- return this.modify((qb) => qb.crossJoinLateral(...args));
775
+ return this.#join("crossJoinLateral", ...args);
701
776
  }
702
777
  };
703
778
  function hydrate(qb, keyBy = DEFAULT_KEY_BY) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kysely-hydrate",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Explicit ORM-style queries with Kysely",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -30,6 +30,7 @@
30
30
  "build": "tsdown",
31
31
  "release": "./scripts/release.sh",
32
32
  "test": "node --test",
33
+ "test:postgres": "RUN_POSTGRES_TESTS=1 node --test src/query-builder.postgres.test.ts",
33
34
  "typecheck": "tsgo",
34
35
  "typecheck:tsc": "tsc",
35
36
  "lint": "oxlint",
@@ -46,12 +47,14 @@
46
47
  "devDependencies": {
47
48
  "@types/better-sqlite3": "^7.6.13",
48
49
  "@types/node": "^22",
50
+ "@types/pg": "^8.16.0",
49
51
  "@typescript/native-preview": "^7.0.0-dev.20251207.1",
50
52
  "better-sqlite3": "^12.4.1",
51
53
  "expect-type": "^1.3.0",
52
54
  "kysely": "^0.28.7",
53
55
  "oxfmt": "^0.16.0",
54
56
  "oxlint": "^1.31.0",
57
+ "pg": "^8.16.3",
55
58
  "postgres-array": "^3.0.4",
56
59
  "tsdown": "^0.17.1",
57
60
  "typescript": "^5.9.3"