kysely-hydrate 0.9.0 → 0.10.1
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 +95 -1
- package/dist/index.d.mts +42 -0
- package/dist/index.mjs +59 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ compromising the power or control of SQL. It offers these features:
|
|
|
36
36
|
- [Application-level joins](#application-level-joins-with-attach)
|
|
37
37
|
- [Mapped fields](#mapped-properties-with-mapfields) in hydrated queries
|
|
38
38
|
- [Computed properties](#computed-properties-with-extras) in hydrated queries
|
|
39
|
-
- [Hydrated writes](#hydrated-writes) (INSERT/UPDATE/DELETE with RETURNING)
|
|
39
|
+
- [Hydrated writes](#hydrated-writes) (INSERT/UPDATE/DELETE with RETURNING, including multi-write CTE orchestration)
|
|
40
40
|
- [Counts, ordering, and limits](#pagination-and-aggregation) accounting for row explosion from nested joins
|
|
41
41
|
|
|
42
42
|
For example:
|
|
@@ -138,6 +138,9 @@ type Result = Array<{
|
|
|
138
138
|
- [Output transformations with `.map()`](#output-transformations-with-map)
|
|
139
139
|
- [Composable mappings with `.with()`](#composable-mappings-with-with)
|
|
140
140
|
- [Hydrated writes](#hydrated-writes)
|
|
141
|
+
- [Initializing with writes](#initializing-with-writes-querysetas)
|
|
142
|
+
- [Reusing query sets for writes](#reusing-query-sets-for-writes)
|
|
143
|
+
- [Multi-write orchestration with `.writeAs()` and `.write()`](#multi-write-orchestration-with-writeas-and-write)
|
|
141
144
|
- [Type helpers](#type-helpers)
|
|
142
145
|
- [Hydrators](#hydrators)
|
|
143
146
|
- [Creating hydrators with `createHydrator()`](#creating-hydrators-with-createhydrator)
|
|
@@ -1169,6 +1172,97 @@ type Result = {
|
|
|
1169
1172
|
};
|
|
1170
1173
|
```
|
|
1171
1174
|
|
|
1175
|
+
#### Multi-write orchestration with `.writeAs()` and `.write()`
|
|
1176
|
+
|
|
1177
|
+
Sometimes a single `INSERT`, `UPDATE`, or `DELETE` isn't enough and you need to
|
|
1178
|
+
orchestrate multiple writes in one statement. For example, updating a user _and_
|
|
1179
|
+
inserting an audit log entry atomically.
|
|
1180
|
+
|
|
1181
|
+
In Postgres, this is done with data-modifying CTEs: a `SELECT` whose `WITH`
|
|
1182
|
+
clause contains `INSERT`, `UPDATE`, or `DELETE` statements. Kysely supports this
|
|
1183
|
+
natively with `.with()`.
|
|
1184
|
+
|
|
1185
|
+
`writeAs()` takes two callbacks. The first builds CTEs and returns a query
|
|
1186
|
+
creator. The second builds the SELECT that references CTE names. The CTEs are
|
|
1187
|
+
placed at the top level of the generated SQL—which is where Postgres requires
|
|
1188
|
+
data-modifying CTEs to live—while the SELECT becomes a derived table with no
|
|
1189
|
+
CTEs to strip.
|
|
1190
|
+
|
|
1191
|
+
```ts
|
|
1192
|
+
const result = await querySet(db)
|
|
1193
|
+
.writeAs(
|
|
1194
|
+
"updated",
|
|
1195
|
+
(db) =>
|
|
1196
|
+
db
|
|
1197
|
+
// Data-modifying CTE: update the user
|
|
1198
|
+
.with("updated", (qb) =>
|
|
1199
|
+
qb
|
|
1200
|
+
.updateTable("users")
|
|
1201
|
+
.set({ email: "new@example.com" })
|
|
1202
|
+
.where("id", "=", userId)
|
|
1203
|
+
.returningAll(),
|
|
1204
|
+
)
|
|
1205
|
+
// Data-modifying CTE: insert an audit log entry
|
|
1206
|
+
.with("audit", (qb) =>
|
|
1207
|
+
qb.insertInto("audit_log").values({ userId, action: "email_changed" }).returning(["id"]),
|
|
1208
|
+
),
|
|
1209
|
+
// Select from the update result
|
|
1210
|
+
(qc) => qc.selectFrom("updated").select(["id", "username", "email"]),
|
|
1211
|
+
)
|
|
1212
|
+
.executeTakeFirstOrThrow();
|
|
1213
|
+
// ⬇
|
|
1214
|
+
type Result = { id: number; username: string; email: string };
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
**Generated SQL:**
|
|
1218
|
+
|
|
1219
|
+
```sql
|
|
1220
|
+
WITH "updated" AS (
|
|
1221
|
+
UPDATE "users" SET "email" = $1 WHERE "id" = $2 RETURNING *
|
|
1222
|
+
),
|
|
1223
|
+
"audit" AS (
|
|
1224
|
+
INSERT INTO "audit_log" ("userId", "action") VALUES ($3, $4) RETURNING "id"
|
|
1225
|
+
)
|
|
1226
|
+
SELECT "updated"."id", "updated"."username", "updated"."email"
|
|
1227
|
+
FROM (SELECT "id", "username", "email" FROM "updated") AS "updated"
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
Like `insertAs` and friends, `writeAs()` supports joins, extras, and all the
|
|
1231
|
+
usual query set features.
|
|
1232
|
+
|
|
1233
|
+
`.write()` is the instance-method equivalent—it switches the base query of an
|
|
1234
|
+
existing query set, just like `.insert()` does for inserts:
|
|
1235
|
+
|
|
1236
|
+
```ts
|
|
1237
|
+
const usersQuerySet = querySet(db)
|
|
1238
|
+
.selectAs("user", db.selectFrom("users").select(["id", "username", "email"]))
|
|
1239
|
+
.leftJoinMany("posts" /* ... */)
|
|
1240
|
+
.extras({ gravatarUrl: (u) => getGravatar(u.email) });
|
|
1241
|
+
|
|
1242
|
+
// Reuse the canonical query set for a select query with data-modifying CTE.
|
|
1243
|
+
const result = await usersQuerySet
|
|
1244
|
+
.write(
|
|
1245
|
+
(db) =>
|
|
1246
|
+
db.with("updated", (qb) =>
|
|
1247
|
+
qb
|
|
1248
|
+
.updateTable("users")
|
|
1249
|
+
.set({ email: "new@example.com" })
|
|
1250
|
+
.where("id", "=", userId)
|
|
1251
|
+
.returningAll(),
|
|
1252
|
+
),
|
|
1253
|
+
(qc) => qc.selectFrom("updated").select(["id", "username", "email"]),
|
|
1254
|
+
)
|
|
1255
|
+
.executeTakeFirstOrThrow();
|
|
1256
|
+
// ⬇ Result includes posts and gravatarUrl!
|
|
1257
|
+
type Result = {
|
|
1258
|
+
id: number;
|
|
1259
|
+
username: string;
|
|
1260
|
+
email: string;
|
|
1261
|
+
gravatarUrl: string;
|
|
1262
|
+
posts: Array<{ id: number; title: string; user_id: number }>;
|
|
1263
|
+
};
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1172
1266
|
### Type Helpers
|
|
1173
1267
|
|
|
1174
1268
|
Kysely Hydrate provides type helpers that mirror
|
package/dist/index.d.mts
CHANGED
|
@@ -1366,6 +1366,18 @@ interface MappedQuerySet<in out T extends TQuerySet> extends k.Compilable, k.Ope
|
|
|
1366
1366
|
* Like {@link insert}, but switches to a `DELETE` statement.
|
|
1367
1367
|
*/
|
|
1368
1368
|
delete<IQB extends k.DeleteQueryBuilder<any, any, T["BaseQuery"]["O"]>>(dqb: DeleteQueryBuilderOrFactory<T["DB"], IQB>): MaybeMappedQuerySet<TWithBaseQuery<T, InferTDeleteQuery<IQB>>>;
|
|
1369
|
+
/**
|
|
1370
|
+
* Switches the base query to a `SELECT` that may contain data-modifying CTEs.
|
|
1371
|
+
*
|
|
1372
|
+
* Callback 1 receives `db`, builds CTEs, and returns a query creator.
|
|
1373
|
+
* Callback 2 receives a query creator typed with the CTE names and builds
|
|
1374
|
+
* the SELECT.
|
|
1375
|
+
*
|
|
1376
|
+
* @param cteFn - Builds CTEs; returns a query creator.
|
|
1377
|
+
* @param selectFn - Builds the SELECT referencing CTE names.
|
|
1378
|
+
* @returns A new QuerySet with the write query as the base.
|
|
1379
|
+
*/
|
|
1380
|
+
write<NewDB, SQB extends k.SelectQueryBuilder<any, any, T["BaseQuery"]["O"]>>(cteFn: (db: k.Kysely<T["DB"]>) => k.QueryCreator<NewDB>, selectFn: (qc: k.QueryCreator<NewDB>) => SQB): MaybeMappedQuerySet<TWithBaseQuery<T, InferTSelectQuery<SQB>>>;
|
|
1369
1381
|
}
|
|
1370
1382
|
/**
|
|
1371
1383
|
* A query set that supports nested joins and automatic hydration.
|
|
@@ -2573,6 +2585,36 @@ declare class QuerySetCreator<in out DB$1> {
|
|
|
2573
2585
|
*/
|
|
2574
2586
|
deleteAs<Alias extends string, DQB extends k.DeleteQueryBuilder<any, any, InputWithDefaultKey>>(alias: Alias, query: DeleteQueryBuilderOrFactory<DB$1, DQB>): InitialQuerySet<DB$1, Alias, InferTDeleteQuery<DQB>>;
|
|
2575
2587
|
deleteAs<Alias extends string, UQB extends AnyDeleteQueryBuilder>(alias: Alias, query: DeleteQueryBuilderOrFactory<DB$1, UQB>, keyBy: KeyBy<InferO<NoInfer<UQB>>>): InitialQuerySet<DB$1, Alias, InferTDeleteQuery<UQB>>;
|
|
2588
|
+
/**
|
|
2589
|
+
* Initializes a new query set with a base `SELECT` query that may contain
|
|
2590
|
+
* data-modifying CTEs.
|
|
2591
|
+
*
|
|
2592
|
+
* Any CTEs on the provided query will be hoisted to the top level of the
|
|
2593
|
+
* generated SQL, which is required by Postgres for data-modifying CTEs.
|
|
2594
|
+
*
|
|
2595
|
+
* This enables multi-write CTE orchestration patterns like:
|
|
2596
|
+
* ```ts
|
|
2597
|
+
* const result = await querySet(db)
|
|
2598
|
+
* .writeAs("updated",
|
|
2599
|
+
* (db) => db.with("updated", (qb) =>
|
|
2600
|
+
* qb.updateTable("users")
|
|
2601
|
+
* .set({ email: "new@example.com" })
|
|
2602
|
+
* .where("id", "=", 1)
|
|
2603
|
+
* .returningAll()
|
|
2604
|
+
* ),
|
|
2605
|
+
* (qc) => qc.selectFrom("updated").selectAll()
|
|
2606
|
+
* )
|
|
2607
|
+
* .executeTakeFirst();
|
|
2608
|
+
* ```
|
|
2609
|
+
*
|
|
2610
|
+
* @param alias - The alias for the base query.
|
|
2611
|
+
* @param cteFn - A callback that receives `db` and builds the CTEs, returning a query creator.
|
|
2612
|
+
* @param selectFn - A callback that receives the query creator and builds the SELECT referencing CTE names.
|
|
2613
|
+
* @param keyBy - The key(s) to uniquely identify rows. Defaults to `"id"`.
|
|
2614
|
+
* @returns A new QuerySet.
|
|
2615
|
+
*/
|
|
2616
|
+
writeAs<Alias extends string, NewDB, SQB extends k.SelectQueryBuilder<any, any, InputWithDefaultKey>>(alias: Alias, cteFn: (db: k.Kysely<DB$1>) => k.QueryCreator<NewDB>, selectFn: (qc: k.QueryCreator<NewDB>) => SQB): InitialQuerySet<DB$1, Alias, InferTSelectQuery<SQB>>;
|
|
2617
|
+
writeAs<Alias extends string, NewDB, SQB extends AnySelectQueryBuilder>(alias: Alias, cteFn: (db: k.Kysely<DB$1>) => k.QueryCreator<NewDB>, selectFn: (qc: k.QueryCreator<NewDB>) => SQB, keyBy: KeyBy<InferO<NoInfer<SQB>>>): InitialQuerySet<DB$1, Alias, InferTSelectQuery<SQB>>;
|
|
2576
2618
|
}
|
|
2577
2619
|
/**
|
|
2578
2620
|
* Creates a new {@link QuerySetCreator} for building query sets with nested joins
|
package/dist/index.mjs
CHANGED
|
@@ -691,6 +691,25 @@ function extractSelectionName(selectionNode) {
|
|
|
691
691
|
* @see {@link QuerySet} - Query builder interface
|
|
692
692
|
* @see {@link MappedQuerySet} - Mapped query builder interface
|
|
693
693
|
*/
|
|
694
|
+
/**
|
|
695
|
+
* A stateless Kysely plugin that strips the WITH clause from a
|
|
696
|
+
* SelectQueryNode. Used to remove CTEs that the query creator
|
|
697
|
+
* attaches to queries built via `selectFn` in `.write()` / `.writeAs()`.
|
|
698
|
+
* The CTEs are already captured separately in `writeQueryCreator`.
|
|
699
|
+
*/
|
|
700
|
+
const stripWithPlugin = {
|
|
701
|
+
transformQuery(args) {
|
|
702
|
+
const node = args.node;
|
|
703
|
+
if (node.kind === "SelectQueryNode" && node.with) return {
|
|
704
|
+
...node,
|
|
705
|
+
with: void 0
|
|
706
|
+
};
|
|
707
|
+
return node;
|
|
708
|
+
},
|
|
709
|
+
async transformResult(args) {
|
|
710
|
+
return args.result;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
694
713
|
const filteringJoins = new Set([
|
|
695
714
|
"innerJoin",
|
|
696
715
|
"innerJoinLateral",
|
|
@@ -733,8 +752,16 @@ var QuerySetImpl = class QuerySetImpl {
|
|
|
733
752
|
return this.#props.baseQuery;
|
|
734
753
|
}
|
|
735
754
|
#getSelectFromBase(isNested, isLocalSubquery) {
|
|
736
|
-
const { db, baseQuery, baseAlias } = this.#props;
|
|
737
|
-
if (isSelectQueryBuilder(baseQuery))
|
|
755
|
+
const { db, baseQuery, baseAlias, writeQueryCreator } = this.#props;
|
|
756
|
+
if (isSelectQueryBuilder(baseQuery)) {
|
|
757
|
+
if (writeQueryCreator) {
|
|
758
|
+
if (isNested) throw new InvalidJoinedQuerySetError(baseAlias);
|
|
759
|
+
let qb$1 = writeQueryCreator.selectFrom(baseQuery.as(baseAlias));
|
|
760
|
+
qb$1 = applyHoistedSelections(qb$1, baseQuery, baseAlias);
|
|
761
|
+
return qb$1;
|
|
762
|
+
}
|
|
763
|
+
return applyHoistedSelections(db.selectFrom(baseQuery.as(baseAlias)), baseQuery, baseAlias);
|
|
764
|
+
}
|
|
738
765
|
if (isNested) throw new InvalidJoinedQuerySetError(baseAlias);
|
|
739
766
|
let qb = (isLocalSubquery ? db : db.with("__base", () => baseQuery)).selectFrom(`__base as ${baseAlias}`);
|
|
740
767
|
if (!isLocalSubquery) return qb.selectAll(baseAlias);
|
|
@@ -1052,6 +1079,14 @@ var QuerySetImpl = class QuerySetImpl {
|
|
|
1052
1079
|
delete(iqb) {
|
|
1053
1080
|
return this.#asWrite(iqb);
|
|
1054
1081
|
}
|
|
1082
|
+
write(cteFn, selectFn) {
|
|
1083
|
+
const qc = cteFn(this.#props.db);
|
|
1084
|
+
const baseQuery = selectFn(qc).withPlugin(stripWithPlugin);
|
|
1085
|
+
return this.#clone({
|
|
1086
|
+
baseQuery,
|
|
1087
|
+
writeQueryCreator: qc
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1055
1090
|
};
|
|
1056
1091
|
/**
|
|
1057
1092
|
* Factory for creating query sets. Obtained by calling {@link querySet}.
|
|
@@ -1078,7 +1113,8 @@ var QuerySetCreator = class {
|
|
|
1078
1113
|
orderBy: [],
|
|
1079
1114
|
orderByKeys: true,
|
|
1080
1115
|
frontModifiers: [],
|
|
1081
|
-
endModifiers: []
|
|
1116
|
+
endModifiers: [],
|
|
1117
|
+
writeQueryCreator: null
|
|
1082
1118
|
});
|
|
1083
1119
|
}
|
|
1084
1120
|
selectAs(alias, query, keyBy = DEFAULT_KEY_BY) {
|
|
@@ -1093,6 +1129,26 @@ var QuerySetCreator = class {
|
|
|
1093
1129
|
deleteAs(alias, query, keyBy = DEFAULT_KEY_BY) {
|
|
1094
1130
|
return this.#createQuerySet(alias, query, keyBy);
|
|
1095
1131
|
}
|
|
1132
|
+
writeAs(alias, cteFn, selectFn, keyBy) {
|
|
1133
|
+
const qc = cteFn(this.#db);
|
|
1134
|
+
const baseQuery = selectFn(qc).withPlugin(stripWithPlugin);
|
|
1135
|
+
return new QuerySetImpl({
|
|
1136
|
+
db: this.#db,
|
|
1137
|
+
baseAlias: alias,
|
|
1138
|
+
baseQuery,
|
|
1139
|
+
keyBy: keyBy ?? DEFAULT_KEY_BY,
|
|
1140
|
+
hydrator: createHydrator().orderByKeys(),
|
|
1141
|
+
joinCollections: /* @__PURE__ */ new Map(),
|
|
1142
|
+
attachCollections: /* @__PURE__ */ new Map(),
|
|
1143
|
+
limit: null,
|
|
1144
|
+
offset: null,
|
|
1145
|
+
orderBy: [],
|
|
1146
|
+
orderByKeys: true,
|
|
1147
|
+
frontModifiers: [],
|
|
1148
|
+
endModifiers: [],
|
|
1149
|
+
writeQueryCreator: qc
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1096
1152
|
};
|
|
1097
1153
|
/**
|
|
1098
1154
|
* Creates a new {@link QuerySetCreator} for building query sets with nested joins
|