kysely-hydrate 0.10.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 CHANGED
@@ -1182,29 +1182,32 @@ In Postgres, this is done with data-modifying CTEs: a `SELECT` whose `WITH`
1182
1182
  clause contains `INSERT`, `UPDATE`, or `DELETE` statements. Kysely supports this
1183
1183
  natively with `.with()`.
1184
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.
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.
1188
1190
 
1189
1191
  ```ts
1190
1192
  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"]),
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"]),
1208
1211
  )
1209
1212
  .executeTakeFirstOrThrow();
1210
1213
  // ⬇
@@ -1238,17 +1241,16 @@ const usersQuerySet = querySet(db)
1238
1241
 
1239
1242
  // Reuse the canonical query set for a select query with data-modifying CTE.
1240
1243
  const result = await usersQuerySet
1241
- .write((db) =>
1242
- db
1243
- .with("updated", (qb) =>
1244
+ .write(
1245
+ (db) =>
1246
+ db.with("updated", (qb) =>
1244
1247
  qb
1245
1248
  .updateTable("users")
1246
1249
  .set({ email: "new@example.com" })
1247
1250
  .where("id", "=", userId)
1248
1251
  .returningAll(),
1249
- )
1250
- .selectFrom("updated")
1251
- .select(["id", "username", "email"]),
1252
+ ),
1253
+ (qc) => qc.selectFrom("updated").select(["id", "username", "email"]),
1252
1254
  )
1253
1255
  .executeTakeFirstOrThrow();
1254
1256
  // ⬇ Result includes posts and gravatarUrl!
package/dist/index.d.mts CHANGED
@@ -1369,22 +1369,15 @@ interface MappedQuerySet<in out T extends TQuerySet> extends k.Compilable, k.Ope
1369
1369
  /**
1370
1370
  * Switches the base query to a `SELECT` that may contain data-modifying CTEs.
1371
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
- * ```
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.
1383
1375
  *
1384
- * @param sqb - A select query builder (possibly with CTEs) or factory function.
1376
+ * @param cteFn - Builds CTEs; returns a query creator.
1377
+ * @param selectFn - Builds the SELECT referencing CTE names.
1385
1378
  * @returns A new QuerySet with the write query as the base.
1386
1379
  */
1387
- write<SQB extends k.SelectQueryBuilder<any, any, T["BaseQuery"]["O"]>>(sqb: WriteQueryBuilderOrFactory<T["DB"], SQB>): MaybeMappedQuerySet<TWithBaseQuery<T, InferTSelectQuery<SQB>>>;
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>>>;
1388
1381
  }
1389
1382
  /**
1390
1383
  * A query set that supports nested joins and automatic hydration.
@@ -2484,8 +2477,6 @@ type UpdateQueryBuilderFactory<InitialDB, UQB extends AnyUpdateQueryBuilder> = (
2484
2477
  type UpdateQueryBuilderOrFactory<InitialDB, UQB extends AnyUpdateQueryBuilder> = UQB | UpdateQueryBuilderFactory<InitialDB, UQB>;
2485
2478
  type DeleteQueryBuilderFactory<InitialDB, DQB extends AnyDeleteQueryBuilder> = (db: DeleteCreator<InitialDB>) => DQB;
2486
2479
  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>;
2489
2480
  interface NestedQuerySetFn<in out DB$1, in out Alias extends string> {
2490
2481
  <SQB extends k.SelectQueryBuilder<any, any, InputWithDefaultKey>>(query: SQB): InitialQuerySet<DB$1, Alias, InferTSelectQuery<SQB>>;
2491
2482
  <SQB extends AnySelectQueryBuilder>(query: SQB, keyBy: KeyBy<InferO<NoInfer<SQB>>>): InitialQuerySet<DB$1, Alias, InferTSelectQuery<SQB>>;
@@ -2604,27 +2595,26 @@ declare class QuerySetCreator<in out DB$1> {
2604
2595
  * This enables multi-write CTE orchestration patterns like:
2605
2596
  * ```ts
2606
2597
  * 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()
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()
2617
2606
  * )
2618
2607
  * .executeTakeFirst();
2619
2608
  * ```
2620
2609
  *
2621
2610
  * @param alias - The alias for the base query.
2622
- * @param query - A Kysely select query builder (possibly with CTEs) or factory function.
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.
2623
2613
  * @param keyBy - The key(s) to uniquely identify rows. Defaults to `"id"`.
2624
2614
  * @returns A new QuerySet.
2625
2615
  */
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>>;
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>>;
2628
2618
  }
2629
2619
  /**
2630
2620
  * Creates a new {@link QuerySetCreator} for building query sets with nested joins
package/dist/index.mjs CHANGED
@@ -609,56 +609,6 @@ 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
-
662
612
  //#endregion
663
613
  //#region src/helpers/select-renamer.ts
664
614
  const fakeQb = k.createSelectQueryBuilder({
@@ -741,6 +691,25 @@ function extractSelectionName(selectionNode) {
741
691
  * @see {@link QuerySet} - Query builder interface
742
692
  * @see {@link MappedQuerySet} - Mapped query builder interface
743
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
+ };
744
713
  const filteringJoins = new Set([
745
714
  "innerJoin",
746
715
  "innerJoinLateral",
@@ -783,17 +752,13 @@ var QuerySetImpl = class QuerySetImpl {
783
752
  return this.#props.baseQuery;
784
753
  }
785
754
  #getSelectFromBase(isNested, isLocalSubquery) {
786
- const { db, baseQuery, baseAlias, hoistCTEs } = this.#props;
755
+ const { db, baseQuery, baseAlias, writeQueryCreator } = this.#props;
787
756
  if (isSelectQueryBuilder(baseQuery)) {
788
- if (hoistCTEs) {
757
+ if (writeQueryCreator) {
789
758
  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
- }
759
+ let qb$1 = writeQueryCreator.selectFrom(baseQuery.as(baseAlias));
760
+ qb$1 = applyHoistedSelections(qb$1, baseQuery, baseAlias);
761
+ return qb$1;
797
762
  }
798
763
  return applyHoistedSelections(db.selectFrom(baseQuery.as(baseAlias)), baseQuery, baseAlias);
799
764
  }
@@ -1114,10 +1079,12 @@ var QuerySetImpl = class QuerySetImpl {
1114
1079
  delete(iqb) {
1115
1080
  return this.#asWrite(iqb);
1116
1081
  }
1117
- write(sqb) {
1082
+ write(cteFn, selectFn) {
1083
+ const qc = cteFn(this.#props.db);
1084
+ const baseQuery = selectFn(qc).withPlugin(stripWithPlugin);
1118
1085
  return this.#clone({
1119
- baseQuery: typeof sqb === "function" ? sqb(this.#props.db) : sqb,
1120
- hoistCTEs: true
1086
+ baseQuery,
1087
+ writeQueryCreator: qc
1121
1088
  });
1122
1089
  }
1123
1090
  };
@@ -1147,7 +1114,7 @@ var QuerySetCreator = class {
1147
1114
  orderByKeys: true,
1148
1115
  frontModifiers: [],
1149
1116
  endModifiers: [],
1150
- hoistCTEs: false
1117
+ writeQueryCreator: null
1151
1118
  });
1152
1119
  }
1153
1120
  selectAs(alias, query, keyBy = DEFAULT_KEY_BY) {
@@ -1162,13 +1129,14 @@ var QuerySetCreator = class {
1162
1129
  deleteAs(alias, query, keyBy = DEFAULT_KEY_BY) {
1163
1130
  return this.#createQuerySet(alias, query, keyBy);
1164
1131
  }
1165
- writeAs(alias, query, keyBy = DEFAULT_KEY_BY) {
1166
- const baseQuery = typeof query === "function" ? query(this.#db) : query;
1132
+ writeAs(alias, cteFn, selectFn, keyBy) {
1133
+ const qc = cteFn(this.#db);
1134
+ const baseQuery = selectFn(qc).withPlugin(stripWithPlugin);
1167
1135
  return new QuerySetImpl({
1168
1136
  db: this.#db,
1169
1137
  baseAlias: alias,
1170
1138
  baseQuery,
1171
- keyBy,
1139
+ keyBy: keyBy ?? DEFAULT_KEY_BY,
1172
1140
  hydrator: createHydrator().orderByKeys(),
1173
1141
  joinCollections: /* @__PURE__ */ new Map(),
1174
1142
  attachCollections: /* @__PURE__ */ new Map(),
@@ -1178,7 +1146,7 @@ var QuerySetCreator = class {
1178
1146
  orderByKeys: true,
1179
1147
  frontModifiers: [],
1180
1148
  endModifiers: [],
1181
- hoistCTEs: true
1149
+ writeQueryCreator: qc
1182
1150
  });
1183
1151
  }
1184
1152
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kysely-hydrate",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Explicit ORM-style queries with Kysely",
5
5
  "homepage": "https://github.com/GiacoCorsiglia/kysely-hydrate#readme",
6
6
  "bugs": {