kysely-hydrate 0.2.1 → 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 +257 -11
- package/dist/index.d.mts +254 -190
- package/dist/index.mjs +167 -152
- 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)
|
|
@@ -138,6 +139,7 @@ By design, Kysely has the following constraints:
|
|
|
138
139
|
- [Mapped properties with `.mapFields()`](#mapped-properties-with-mapfields)
|
|
139
140
|
- [Computed properties with `.extras()`](#computed-properties-with-extras)
|
|
140
141
|
- [Excluded properties with `.omit()`](#excluded-properties-with-omit)
|
|
142
|
+
- [Output transformations with `.map()`](#output-transformations-with-map)
|
|
141
143
|
- [Composable mappings with `.with()`](#composable-mappings-with-with)
|
|
142
144
|
- [Execution](#execution)
|
|
143
145
|
- [Hydrators](#hydrators)
|
|
@@ -146,6 +148,7 @@ By design, Kysely has the following constraints:
|
|
|
146
148
|
- [Selecting and mapping fields with `.fields()`](#selecting-and-mapping-fields-with-fields)
|
|
147
149
|
- [Computed properties with `.extras()`](#computed-properties-with-extras-1)
|
|
148
150
|
- [Excluding fields with `.omit()`](#excluding-fields-with-omit)
|
|
151
|
+
- [Output transformations with `.map()`](#output-transformations-with-map-1)
|
|
149
152
|
- [Attached collections with `.attach*()`](#attached-collections-with-attach)
|
|
150
153
|
- [Prefixed collections with `.has*()`](#prefixed-collections-with-has)
|
|
151
154
|
- [Composing hydrators with `.extend()`](#composing-hydrators-with-extend)
|
|
@@ -463,12 +466,86 @@ but you know the record must exist.
|
|
|
463
466
|
|
|
464
467
|
> [!NOTE]
|
|
465
468
|
> Like `hydrate()`, `hasOneOrThrow()` also accepts an optional final `keyBy` argument with the
|
|
466
|
-
> 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()`.
|
|
467
542
|
|
|
468
543
|
#### SQL output
|
|
469
544
|
|
|
470
545
|
Kysely Hydrate produces the SQL you tell it to with exactly one exception: it
|
|
471
|
-
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.)
|
|
472
549
|
|
|
473
550
|
For example, `users.id` remains `users.id`, but `posts.id` nested under "posts"
|
|
474
551
|
becomes `posts$$id`. The hydration layer then un-flattens these aliases back
|
|
@@ -779,6 +856,134 @@ const users = await hydrate(
|
|
|
779
856
|
type Result = Array<{ id: number; fullName: string }>;
|
|
780
857
|
```
|
|
781
858
|
|
|
859
|
+
### Output transformations with `.map()`
|
|
860
|
+
|
|
861
|
+
The `.map()` method transforms the hydrated output into a different shape. Use
|
|
862
|
+
it for complex transformations like:
|
|
863
|
+
|
|
864
|
+
- Converting plain objects into class instances
|
|
865
|
+
- Asserting discriminated union types
|
|
866
|
+
- Restructuring or reshaping data
|
|
867
|
+
|
|
868
|
+
Unlike `.mapFields()` and `.extras()`, which operate on individual fields,
|
|
869
|
+
`.map()` receives the complete hydrated result and can transform it however you
|
|
870
|
+
want.
|
|
871
|
+
|
|
872
|
+
```ts
|
|
873
|
+
// Transform into class instances
|
|
874
|
+
class UserModel {
|
|
875
|
+
constructor(
|
|
876
|
+
public id: number,
|
|
877
|
+
public name: string,
|
|
878
|
+
) {}
|
|
879
|
+
|
|
880
|
+
getDisplayName() {
|
|
881
|
+
return `User: ${this.name}`;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const users = await hydrate(
|
|
886
|
+
db.selectFrom("users").select(["users.id", "users.name"]),
|
|
887
|
+
)
|
|
888
|
+
.map((user) => new UserModel(user.id, user.name))
|
|
889
|
+
.execute();
|
|
890
|
+
// ⬇
|
|
891
|
+
type Result = UserModel[];
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
#### Chaining transformations
|
|
895
|
+
|
|
896
|
+
You can chain multiple `.map()` calls to compose transformations. Each function
|
|
897
|
+
receives the output of the previous transformation.
|
|
898
|
+
|
|
899
|
+
```ts
|
|
900
|
+
const users = await hydrate(
|
|
901
|
+
db.selectFrom("users").select(["users.id", "users.name"]),
|
|
902
|
+
)
|
|
903
|
+
.map((user) => ({ ...user, nameUpper: user.name.toUpperCase() }))
|
|
904
|
+
.map((user) => ({ id: user.id, display: user.nameUpper }))
|
|
905
|
+
.execute();
|
|
906
|
+
// ⬇
|
|
907
|
+
type Result = Array<{ id: number; display: string }>;
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
#### Transforming nested collections
|
|
911
|
+
|
|
912
|
+
`.map()` works with nested collections too. You can apply transformations to
|
|
913
|
+
child entities and then to parents:
|
|
914
|
+
|
|
915
|
+
```ts
|
|
916
|
+
const users = await hydrate(db.selectFrom("users").select(["users.id"]))
|
|
917
|
+
.hasMany("posts", ({ leftJoin }) =>
|
|
918
|
+
leftJoin("posts", "posts.userId", "users.id")
|
|
919
|
+
.select(["posts.id", "posts.title"])
|
|
920
|
+
// Transform child:
|
|
921
|
+
.map((post) => ({ postId: post.id, postTitle: post.title })),
|
|
922
|
+
)
|
|
923
|
+
// Transform parent:
|
|
924
|
+
.map((user) => ({
|
|
925
|
+
userId: user.id,
|
|
926
|
+
postCount: user.posts.length,
|
|
927
|
+
posts: user.posts,
|
|
928
|
+
}))
|
|
929
|
+
.execute();
|
|
930
|
+
// ⬇
|
|
931
|
+
type Result = Array<{
|
|
932
|
+
userId: number;
|
|
933
|
+
postCount: number;
|
|
934
|
+
posts: {
|
|
935
|
+
postId: number;
|
|
936
|
+
postTitle: string;
|
|
937
|
+
};
|
|
938
|
+
}>;
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
#### Terminal operation
|
|
942
|
+
|
|
943
|
+
`.map()` is a **terminal operation**. After you call `.map()`, you can only:
|
|
944
|
+
|
|
945
|
+
- Call `.map()` again to chain another transformation
|
|
946
|
+
- Call `.modify()` to adjust the underlying SQL query
|
|
947
|
+
- Call execution methods (`.execute()`, `.executeTakeFirst()`, etc.)
|
|
948
|
+
|
|
949
|
+
You **cannot** call configuration methods like `.mapFields()`, `.extras()`,
|
|
950
|
+
`.hasMany()`, or `.with()` after `.map()`.
|
|
951
|
+
|
|
952
|
+
This is intentional: those methods would affect the *input* type expected by
|
|
953
|
+
your transformation function, which would break your mapping logic. By
|
|
954
|
+
preventing further configuration, the type system protects you from this class
|
|
955
|
+
of bugs.
|
|
956
|
+
|
|
957
|
+
```ts
|
|
958
|
+
const mapped = hydrate(db.selectFrom("users").select(["users.id"]))
|
|
959
|
+
.map((user) => ({ userId: user.id }));
|
|
960
|
+
|
|
961
|
+
// ✅ These work:
|
|
962
|
+
mapped.map((data) => ({ transformed: data.userId }));
|
|
963
|
+
mapped.execute();
|
|
964
|
+
|
|
965
|
+
// ❌ These don't compile:
|
|
966
|
+
mapped.mapFields({ ... }); // Error: Property 'mapFields' does not exist
|
|
967
|
+
mapped.hasMany(...); // Error: Property 'hasMany' does not exist
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
#### `.map()` vs `.mapFields()` and `.extras()`
|
|
971
|
+
|
|
972
|
+
When should you use `.map()` vs the more targeted methods?
|
|
973
|
+
|
|
974
|
+
- **Use `.mapFields()`** when you want to transform individual fields by name
|
|
975
|
+
(e.g., normalizing strings)
|
|
976
|
+
- **Use `.extras()`** when you want to add computed fields while keeping the
|
|
977
|
+
existing structure
|
|
978
|
+
- **Use `.map()`** when you need to:
|
|
979
|
+
- Convert to class instances
|
|
980
|
+
- Completely reshape the output
|
|
981
|
+
- Apply transformations that depend on the full hydrated result
|
|
982
|
+
- Assert or narrow types on the entire output shape
|
|
983
|
+
|
|
984
|
+
The targeted methods are more composable, because they can be interleaved with
|
|
985
|
+
joins, unlike `.map()`.
|
|
986
|
+
|
|
782
987
|
### Composable mappings with `.with()`
|
|
783
988
|
|
|
784
989
|
Re-use hydration logic by importing it from another `Hydrator`. This is great for
|
|
@@ -948,20 +1153,16 @@ Computes new fields from the input row.
|
|
|
948
1153
|
type UserRow = { id: number; username: string; email: string };
|
|
949
1154
|
|
|
950
1155
|
const hydrator = createHydrator<UserRow>()
|
|
951
|
-
.fields({ id: true, username: true, email: true })
|
|
952
1156
|
.extras({
|
|
953
1157
|
displayName: (u) => `${u.username} <${u.email}>`,
|
|
954
1158
|
})
|
|
955
|
-
.omit(["email"]);
|
|
956
1159
|
// ⬇
|
|
957
|
-
type Result = Array<{
|
|
1160
|
+
type Result = Array<{ displayName: string }>;
|
|
958
1161
|
```
|
|
959
1162
|
|
|
960
1163
|
### Excluding fields with `.omit()`
|
|
961
1164
|
|
|
962
|
-
Excludes fields from the output that were already included.
|
|
963
|
-
primarily exists for use by the `HydratedQueryBuilder`, which includes all
|
|
964
|
-
fields by default.
|
|
1165
|
+
Excludes fields from the output that were already included.
|
|
965
1166
|
|
|
966
1167
|
```ts
|
|
967
1168
|
type UserRow = { id: number; passwordHash: string };
|
|
@@ -973,6 +1174,53 @@ const hydrator = createHydrator<UserRow>()
|
|
|
973
1174
|
type Result = Array<{ id: number }>;
|
|
974
1175
|
```
|
|
975
1176
|
|
|
1177
|
+
This method primarily exists for use by the `HydratedQueryBuilder`, which
|
|
1178
|
+
includes all fields by default. It's not so useful in standalone Hydrators, in
|
|
1179
|
+
which you must explicitly name the fields to include. The example above is
|
|
1180
|
+
equivalent to `createHydrator<UserRow>().fields({ id: true })`.
|
|
1181
|
+
|
|
1182
|
+
### Output transformations with `.map()`
|
|
1183
|
+
|
|
1184
|
+
The `.map()` method works the same way as described in the
|
|
1185
|
+
[`hydrate()` API section](#output-transformations-with-map) above: it
|
|
1186
|
+
transforms the hydrated output into a different shape, such as class instances
|
|
1187
|
+
or discriminated union types.
|
|
1188
|
+
|
|
1189
|
+
```ts
|
|
1190
|
+
class UserModel {
|
|
1191
|
+
constructor(
|
|
1192
|
+
public id: number,
|
|
1193
|
+
public name: string,
|
|
1194
|
+
) {}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const hydrator = createHydrator<{ id: number; name: string }>()
|
|
1198
|
+
.fields({ id: true, name: true })
|
|
1199
|
+
.map((user) => new UserModel(user.id, user.name));
|
|
1200
|
+
|
|
1201
|
+
const users = await hydrateData(rows, hydrator);
|
|
1202
|
+
// ⬇
|
|
1203
|
+
type Result = UserModel[];
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
Like with `HydratedQueryBuilder`, `.map()` is a terminal operation—after
|
|
1207
|
+
calling it, you can only call `.map()` again or `.hydrate()`. You cannot call
|
|
1208
|
+
configuration methods like `.fields()`, `.extras()`, `.has*()`, or `.extend()`.
|
|
1209
|
+
|
|
1210
|
+
```ts
|
|
1211
|
+
const mapped = createHydrator<User>()
|
|
1212
|
+
.fields({ id: true })
|
|
1213
|
+
.map((u) => ({ userId: u.id }));
|
|
1214
|
+
|
|
1215
|
+
// ✅ These work:
|
|
1216
|
+
mapped.map((data) => ({ transformed: data.userId }));
|
|
1217
|
+
mapped.hydrate(rows);
|
|
1218
|
+
|
|
1219
|
+
// ❌ These don't compile:
|
|
1220
|
+
mapped.fields({ ... }); // Error: Property 'fields' does not exist
|
|
1221
|
+
mapped.extend(...); // Error: Property 'extend' does not exist
|
|
1222
|
+
```
|
|
1223
|
+
|
|
976
1224
|
### Attached collections with `.attach*()`
|
|
977
1225
|
|
|
978
1226
|
These work the same as in the `hydrate()` API (see the `.attach*()` section above).
|
|
@@ -1041,9 +1289,7 @@ type UserRow = { id: number; username: string; email: string };
|
|
|
1041
1289
|
const base = createHydrator<UserRow>().fields({ id: true, username: true });
|
|
1042
1290
|
|
|
1043
1291
|
const withDisplayName = createHydrator<UserRow>()
|
|
1044
|
-
.
|
|
1045
|
-
.extras({ displayName: (u) => `${u.username} <${u.email}>` })
|
|
1046
|
-
.omit(["email"]);
|
|
1292
|
+
.extras({ displayName: (u) => `${u.username} <${u.email}>` });
|
|
1047
1293
|
|
|
1048
1294
|
const combined = base.extend(withDisplayName);
|
|
1049
1295
|
// ⬇
|