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 +77 -2
- package/dist/index.d.mts +86 -8
- package/dist/index.mjs +88 -13
- package/package.json +4 -1
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
|
|
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
|
|
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
|
|
779
|
-
* `
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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.#
|
|
705
|
+
return this.#has("many", key, jb, keyBy);
|
|
654
706
|
}
|
|
655
707
|
hasOne(key, jb, keyBy) {
|
|
656
|
-
return this.#
|
|
708
|
+
return this.#has("one", key, jb, keyBy);
|
|
657
709
|
}
|
|
658
710
|
hasOneOrThrow(key, jb, keyBy) {
|
|
659
|
-
return this.#
|
|
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(
|
|
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
|
|
760
|
+
return this.#join("innerJoin", ...args);
|
|
686
761
|
}
|
|
687
762
|
leftJoin(...args) {
|
|
688
|
-
return this
|
|
763
|
+
return this.#join("leftJoin", ...args);
|
|
689
764
|
}
|
|
690
765
|
crossJoin(...args) {
|
|
691
|
-
return this
|
|
766
|
+
return this.#join("crossJoin", ...args);
|
|
692
767
|
}
|
|
693
768
|
innerJoinLateral(...args) {
|
|
694
|
-
return this
|
|
769
|
+
return this.#join("innerJoinLateral", ...args);
|
|
695
770
|
}
|
|
696
771
|
leftJoinLateral(...args) {
|
|
697
|
-
return this
|
|
772
|
+
return this.#join("leftJoinLateral", ...args);
|
|
698
773
|
}
|
|
699
774
|
crossJoinLateral(...args) {
|
|
700
|
-
return this
|
|
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
|
+
"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"
|