kysely-hydrate 0.9.0 → 0.10.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 +93 -1
- package/dist/index.d.mts +52 -0
- package/dist/index.mjs +91 -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,95 @@ 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()` initializes a query set from such a query. Any CTEs on the provided
|
|
1186
|
+
`SELECT` are hoisted to the top level of the generated SQL—which is where
|
|
1187
|
+
Postgres requires data-modifying CTEs to live.
|
|
1188
|
+
|
|
1189
|
+
```ts
|
|
1190
|
+
const result = await querySet(db)
|
|
1191
|
+
.writeAs("updated", (db) =>
|
|
1192
|
+
db
|
|
1193
|
+
// Data-modifying CTE: update the user
|
|
1194
|
+
.with("updated", (qb) =>
|
|
1195
|
+
qb
|
|
1196
|
+
.updateTable("users")
|
|
1197
|
+
.set({ email: "new@example.com" })
|
|
1198
|
+
.where("id", "=", userId)
|
|
1199
|
+
.returningAll(),
|
|
1200
|
+
)
|
|
1201
|
+
// Data-modifying CTE: insert an audit log entry
|
|
1202
|
+
.with("audit", (qb) =>
|
|
1203
|
+
qb.insertInto("audit_log").values({ userId, action: "email_changed" }).returning(["id"]),
|
|
1204
|
+
)
|
|
1205
|
+
// Select from the update result
|
|
1206
|
+
.selectFrom("updated")
|
|
1207
|
+
.select(["id", "username", "email"]),
|
|
1208
|
+
)
|
|
1209
|
+
.executeTakeFirstOrThrow();
|
|
1210
|
+
// ⬇
|
|
1211
|
+
type Result = { id: number; username: string; email: string };
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
**Generated SQL:**
|
|
1215
|
+
|
|
1216
|
+
```sql
|
|
1217
|
+
WITH "updated" AS (
|
|
1218
|
+
UPDATE "users" SET "email" = $1 WHERE "id" = $2 RETURNING *
|
|
1219
|
+
),
|
|
1220
|
+
"audit" AS (
|
|
1221
|
+
INSERT INTO "audit_log" ("userId", "action") VALUES ($3, $4) RETURNING "id"
|
|
1222
|
+
)
|
|
1223
|
+
SELECT "updated"."id", "updated"."username", "updated"."email"
|
|
1224
|
+
FROM (SELECT "id", "username", "email" FROM "updated") AS "updated"
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
Like `insertAs` and friends, `writeAs()` supports joins, extras, and all the
|
|
1228
|
+
usual query set features.
|
|
1229
|
+
|
|
1230
|
+
`.write()` is the instance-method equivalent—it switches the base query of an
|
|
1231
|
+
existing query set, just like `.insert()` does for inserts:
|
|
1232
|
+
|
|
1233
|
+
```ts
|
|
1234
|
+
const usersQuerySet = querySet(db)
|
|
1235
|
+
.selectAs("user", db.selectFrom("users").select(["id", "username", "email"]))
|
|
1236
|
+
.leftJoinMany("posts" /* ... */)
|
|
1237
|
+
.extras({ gravatarUrl: (u) => getGravatar(u.email) });
|
|
1238
|
+
|
|
1239
|
+
// Reuse the canonical query set for a select query with data-modifying CTE.
|
|
1240
|
+
const result = await usersQuerySet
|
|
1241
|
+
.write((db) =>
|
|
1242
|
+
db
|
|
1243
|
+
.with("updated", (qb) =>
|
|
1244
|
+
qb
|
|
1245
|
+
.updateTable("users")
|
|
1246
|
+
.set({ email: "new@example.com" })
|
|
1247
|
+
.where("id", "=", userId)
|
|
1248
|
+
.returningAll(),
|
|
1249
|
+
)
|
|
1250
|
+
.selectFrom("updated")
|
|
1251
|
+
.select(["id", "username", "email"]),
|
|
1252
|
+
)
|
|
1253
|
+
.executeTakeFirstOrThrow();
|
|
1254
|
+
// ⬇ Result includes posts and gravatarUrl!
|
|
1255
|
+
type Result = {
|
|
1256
|
+
id: number;
|
|
1257
|
+
username: string;
|
|
1258
|
+
email: string;
|
|
1259
|
+
gravatarUrl: string;
|
|
1260
|
+
posts: Array<{ id: number; title: string; user_id: number }>;
|
|
1261
|
+
};
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1172
1264
|
### Type Helpers
|
|
1173
1265
|
|
|
1174
1266
|
Kysely Hydrate provides type helpers that mirror
|
package/dist/index.d.mts
CHANGED
|
@@ -1366,6 +1366,25 @@ 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
|
+
* Any CTEs on the provided query will be hoisted to the top level of the
|
|
1373
|
+
* generated SQL, which is required by Postgres for data-modifying CTEs.
|
|
1374
|
+
* The stripped `SELECT` is inlined as a derived table, just like
|
|
1375
|
+
* {@link selectAs}.
|
|
1376
|
+
*
|
|
1377
|
+
* This enables multi-write CTE orchestration patterns like:
|
|
1378
|
+
* ```sql
|
|
1379
|
+
* WITH "updated" AS (UPDATE ... RETURNING *),
|
|
1380
|
+
* "audit" AS (INSERT INTO audit_log ... RETURNING *)
|
|
1381
|
+
* SELECT * FROM "updated"
|
|
1382
|
+
* ```
|
|
1383
|
+
*
|
|
1384
|
+
* @param sqb - A select query builder (possibly with CTEs) or factory function.
|
|
1385
|
+
* @returns A new QuerySet with the write query as the base.
|
|
1386
|
+
*/
|
|
1387
|
+
write<SQB extends k.SelectQueryBuilder<any, any, T["BaseQuery"]["O"]>>(sqb: WriteQueryBuilderOrFactory<T["DB"], SQB>): MaybeMappedQuerySet<TWithBaseQuery<T, InferTSelectQuery<SQB>>>;
|
|
1369
1388
|
}
|
|
1370
1389
|
/**
|
|
1371
1390
|
* A query set that supports nested joins and automatic hydration.
|
|
@@ -2465,6 +2484,8 @@ type UpdateQueryBuilderFactory<InitialDB, UQB extends AnyUpdateQueryBuilder> = (
|
|
|
2465
2484
|
type UpdateQueryBuilderOrFactory<InitialDB, UQB extends AnyUpdateQueryBuilder> = UQB | UpdateQueryBuilderFactory<InitialDB, UQB>;
|
|
2466
2485
|
type DeleteQueryBuilderFactory<InitialDB, DQB extends AnyDeleteQueryBuilder> = (db: DeleteCreator<InitialDB>) => DQB;
|
|
2467
2486
|
type DeleteQueryBuilderOrFactory<InitialDB, DQB extends AnyDeleteQueryBuilder> = DQB | DeleteQueryBuilderFactory<InitialDB, DQB>;
|
|
2487
|
+
type WriteQueryBuilderFactory<InitialDB, SQB extends AnySelectQueryBuilder> = (db: k.Kysely<InitialDB>) => SQB;
|
|
2488
|
+
type WriteQueryBuilderOrFactory<InitialDB, SQB extends AnySelectQueryBuilder> = SQB | WriteQueryBuilderFactory<InitialDB, SQB>;
|
|
2468
2489
|
interface NestedQuerySetFn<in out DB$1, in out Alias extends string> {
|
|
2469
2490
|
<SQB extends k.SelectQueryBuilder<any, any, InputWithDefaultKey>>(query: SQB): InitialQuerySet<DB$1, Alias, InferTSelectQuery<SQB>>;
|
|
2470
2491
|
<SQB extends AnySelectQueryBuilder>(query: SQB, keyBy: KeyBy<InferO<NoInfer<SQB>>>): InitialQuerySet<DB$1, Alias, InferTSelectQuery<SQB>>;
|
|
@@ -2573,6 +2594,37 @@ declare class QuerySetCreator<in out DB$1> {
|
|
|
2573
2594
|
*/
|
|
2574
2595
|
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
2596
|
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>>;
|
|
2597
|
+
/**
|
|
2598
|
+
* Initializes a new query set with a base `SELECT` query that may contain
|
|
2599
|
+
* data-modifying CTEs.
|
|
2600
|
+
*
|
|
2601
|
+
* Any CTEs on the provided query will be hoisted to the top level of the
|
|
2602
|
+
* generated SQL, which is required by Postgres for data-modifying CTEs.
|
|
2603
|
+
*
|
|
2604
|
+
* This enables multi-write CTE orchestration patterns like:
|
|
2605
|
+
* ```ts
|
|
2606
|
+
* const result = await querySet(db)
|
|
2607
|
+
* .writeAs("updated", (db) =>
|
|
2608
|
+
* db
|
|
2609
|
+
* .with("updated", (qb) =>
|
|
2610
|
+
* qb.updateTable("users")
|
|
2611
|
+
* .set({ email: "new@example.com" })
|
|
2612
|
+
* .where("id", "=", 1)
|
|
2613
|
+
* .returningAll()
|
|
2614
|
+
* )
|
|
2615
|
+
* .selectFrom("updated")
|
|
2616
|
+
* .selectAll()
|
|
2617
|
+
* )
|
|
2618
|
+
* .executeTakeFirst();
|
|
2619
|
+
* ```
|
|
2620
|
+
*
|
|
2621
|
+
* @param alias - The alias for the base query.
|
|
2622
|
+
* @param query - A Kysely select query builder (possibly with CTEs) or factory function.
|
|
2623
|
+
* @param keyBy - The key(s) to uniquely identify rows. Defaults to `"id"`.
|
|
2624
|
+
* @returns A new QuerySet.
|
|
2625
|
+
*/
|
|
2626
|
+
writeAs<Alias extends string, SQB extends k.SelectQueryBuilder<any, any, InputWithDefaultKey>>(alias: Alias, query: WriteQueryBuilderOrFactory<DB$1, SQB>): InitialQuerySet<DB$1, Alias, InferTSelectQuery<SQB>>;
|
|
2627
|
+
writeAs<Alias extends string, SQB extends AnySelectQueryBuilder>(alias: Alias, query: WriteQueryBuilderOrFactory<DB$1, SQB>, keyBy: KeyBy<InferO<NoInfer<SQB>>>): InitialQuerySet<DB$1, Alias, InferTSelectQuery<SQB>>;
|
|
2576
2628
|
}
|
|
2577
2629
|
/**
|
|
2578
2630
|
* Creates a new {@link QuerySetCreator} for building query sets with nested joins
|
package/dist/index.mjs
CHANGED
|
@@ -609,6 +609,56 @@ function groupByKey(prefix, inputs, keyBy) {
|
|
|
609
609
|
return map;
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region src/helpers/cte-hoisting.ts
|
|
614
|
+
/**
|
|
615
|
+
* Extracts the CTE expressions from a query builder's operation node.
|
|
616
|
+
*/
|
|
617
|
+
function extractCTEs(qb) {
|
|
618
|
+
return qb.toOperationNode().with?.expressions;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* A Kysely plugin that strips the WITH clause from a SelectQueryNode,
|
|
622
|
+
* effectively removing all CTEs from the query.
|
|
623
|
+
*/
|
|
624
|
+
var StripWithPlugin = class {
|
|
625
|
+
transformQuery(args) {
|
|
626
|
+
const node = args.node;
|
|
627
|
+
if (node.kind === "SelectQueryNode" && node.with) return {
|
|
628
|
+
...node,
|
|
629
|
+
with: void 0
|
|
630
|
+
};
|
|
631
|
+
return node;
|
|
632
|
+
}
|
|
633
|
+
async transformResult(args) {
|
|
634
|
+
return args.result;
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* A Kysely plugin that prepends CTE expressions to the outer query's WithNode.
|
|
639
|
+
*/
|
|
640
|
+
var AddCTEsPlugin = class {
|
|
641
|
+
#expressions;
|
|
642
|
+
constructor(expressions) {
|
|
643
|
+
this.#expressions = expressions;
|
|
644
|
+
}
|
|
645
|
+
transformQuery(args) {
|
|
646
|
+
const node = args.node;
|
|
647
|
+
if (node.kind !== "SelectQueryNode") return node;
|
|
648
|
+
const existing = node.with?.expressions ?? [];
|
|
649
|
+
const merged = [...this.#expressions, ...existing];
|
|
650
|
+
let withNode = k.WithNode.create(merged[0]);
|
|
651
|
+
for (let i = 1; i < merged.length; i++) withNode = k.WithNode.cloneWithExpression(withNode, merged[i]);
|
|
652
|
+
return {
|
|
653
|
+
...node,
|
|
654
|
+
with: withNode
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
async transformResult(args) {
|
|
658
|
+
return args.result;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
612
662
|
//#endregion
|
|
613
663
|
//#region src/helpers/select-renamer.ts
|
|
614
664
|
const fakeQb = k.createSelectQueryBuilder({
|
|
@@ -733,8 +783,20 @@ var QuerySetImpl = class QuerySetImpl {
|
|
|
733
783
|
return this.#props.baseQuery;
|
|
734
784
|
}
|
|
735
785
|
#getSelectFromBase(isNested, isLocalSubquery) {
|
|
736
|
-
const { db, baseQuery, baseAlias } = this.#props;
|
|
737
|
-
if (isSelectQueryBuilder(baseQuery))
|
|
786
|
+
const { db, baseQuery, baseAlias, hoistCTEs } = this.#props;
|
|
787
|
+
if (isSelectQueryBuilder(baseQuery)) {
|
|
788
|
+
if (hoistCTEs) {
|
|
789
|
+
if (isNested) throw new InvalidJoinedQuerySetError(baseAlias);
|
|
790
|
+
const ctes = extractCTEs(baseQuery);
|
|
791
|
+
if (ctes?.length) {
|
|
792
|
+
const stripped = baseQuery.withPlugin(new StripWithPlugin());
|
|
793
|
+
let qb$1 = db.selectFrom(stripped.as(baseAlias));
|
|
794
|
+
qb$1 = applyHoistedSelections(qb$1, baseQuery, baseAlias);
|
|
795
|
+
return qb$1.withPlugin(new AddCTEsPlugin(ctes));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return applyHoistedSelections(db.selectFrom(baseQuery.as(baseAlias)), baseQuery, baseAlias);
|
|
799
|
+
}
|
|
738
800
|
if (isNested) throw new InvalidJoinedQuerySetError(baseAlias);
|
|
739
801
|
let qb = (isLocalSubquery ? db : db.with("__base", () => baseQuery)).selectFrom(`__base as ${baseAlias}`);
|
|
740
802
|
if (!isLocalSubquery) return qb.selectAll(baseAlias);
|
|
@@ -1052,6 +1114,12 @@ var QuerySetImpl = class QuerySetImpl {
|
|
|
1052
1114
|
delete(iqb) {
|
|
1053
1115
|
return this.#asWrite(iqb);
|
|
1054
1116
|
}
|
|
1117
|
+
write(sqb) {
|
|
1118
|
+
return this.#clone({
|
|
1119
|
+
baseQuery: typeof sqb === "function" ? sqb(this.#props.db) : sqb,
|
|
1120
|
+
hoistCTEs: true
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1055
1123
|
};
|
|
1056
1124
|
/**
|
|
1057
1125
|
* Factory for creating query sets. Obtained by calling {@link querySet}.
|
|
@@ -1078,7 +1146,8 @@ var QuerySetCreator = class {
|
|
|
1078
1146
|
orderBy: [],
|
|
1079
1147
|
orderByKeys: true,
|
|
1080
1148
|
frontModifiers: [],
|
|
1081
|
-
endModifiers: []
|
|
1149
|
+
endModifiers: [],
|
|
1150
|
+
hoistCTEs: false
|
|
1082
1151
|
});
|
|
1083
1152
|
}
|
|
1084
1153
|
selectAs(alias, query, keyBy = DEFAULT_KEY_BY) {
|
|
@@ -1093,6 +1162,25 @@ var QuerySetCreator = class {
|
|
|
1093
1162
|
deleteAs(alias, query, keyBy = DEFAULT_KEY_BY) {
|
|
1094
1163
|
return this.#createQuerySet(alias, query, keyBy);
|
|
1095
1164
|
}
|
|
1165
|
+
writeAs(alias, query, keyBy = DEFAULT_KEY_BY) {
|
|
1166
|
+
const baseQuery = typeof query === "function" ? query(this.#db) : query;
|
|
1167
|
+
return new QuerySetImpl({
|
|
1168
|
+
db: this.#db,
|
|
1169
|
+
baseAlias: alias,
|
|
1170
|
+
baseQuery,
|
|
1171
|
+
keyBy,
|
|
1172
|
+
hydrator: createHydrator().orderByKeys(),
|
|
1173
|
+
joinCollections: /* @__PURE__ */ new Map(),
|
|
1174
|
+
attachCollections: /* @__PURE__ */ new Map(),
|
|
1175
|
+
limit: null,
|
|
1176
|
+
offset: null,
|
|
1177
|
+
orderBy: [],
|
|
1178
|
+
orderByKeys: true,
|
|
1179
|
+
frontModifiers: [],
|
|
1180
|
+
endModifiers: [],
|
|
1181
|
+
hoistCTEs: true
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1096
1184
|
};
|
|
1097
1185
|
/**
|
|
1098
1186
|
* Creates a new {@link QuerySetCreator} for building query sets with nested joins
|