kysely-hydrate 0.10.0 → 0.10.2

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
  }
@@ -842,12 +807,16 @@ var QuerySetImpl = class QuerySetImpl {
842
807
  if (offset !== null) qb = qb.offset(offset);
843
808
  return qb;
844
809
  }
845
- #applyOrderBy(qb) {
810
+ #applyOrderBy(qb, isOuter = false) {
846
811
  const { baseAlias, keyBy, orderBy } = this.#props;
812
+ const eb = k.expressionBuilder(qb);
847
813
  let keyByArray = typeof keyBy === "string" ? [keyBy] : keyBy;
848
814
  for (const { expr, modifiers } of orderBy) {
849
- const sqlExpr = expr.includes(SEP) ? expr.replace(SEP, ".") : `${baseAlias}.${expr}`;
850
- qb = qb.orderBy(sqlExpr, modifiers);
815
+ let orderExpr;
816
+ if (expr.includes(SEP)) if (isOuter) orderExpr = eb.ref(`${baseAlias}.${expr}`);
817
+ else orderExpr = expr.replace(SEP, ".");
818
+ else orderExpr = `${baseAlias}.${expr}`;
819
+ qb = qb.orderBy(orderExpr, modifiers);
851
820
  keyByArray = keyByArray.filter((k$1) => k$1 !== expr);
852
821
  }
853
822
  if (this.#props.orderByKeys) for (const key of keyByArray) qb = qb.orderBy(`${baseAlias}.${key}`, "asc");
@@ -873,7 +842,7 @@ var QuerySetImpl = class QuerySetImpl {
873
842
  const { joinCollections } = this.#props;
874
843
  let qb = this.#getSelectFromBase(isNested, isLocalSubquery);
875
844
  for (const [key, collection] of joinCollections) qb = this.#addCollectionAsJoin(qb, key, collection);
876
- if (!(isNested || isLocalSubquery)) qb = this.#applyOrderBy(qb);
845
+ if (!(isNested || isLocalSubquery)) qb = this.#applyOrderBy(qb, false);
877
846
  return qb;
878
847
  }
879
848
  toJoinedQuery() {
@@ -893,12 +862,12 @@ var QuerySetImpl = class QuerySetImpl {
893
862
  if (this.#isCardinalityOne()) return this.#applyLimitAndOffset(this.#toJoinedQuery(isNested, isLocalSubquery));
894
863
  let cardinalityOneQuery = this.#toCardinalityOneQuery(isNested, isLocalSubquery);
895
864
  cardinalityOneQuery = this.#applyLimitAndOffset(cardinalityOneQuery);
896
- if (limit || offset) cardinalityOneQuery = this.#applyOrderBy(cardinalityOneQuery);
865
+ if (limit || offset) cardinalityOneQuery = this.#applyOrderBy(cardinalityOneQuery, false);
897
866
  const aliasedCardinalityOneQuery = cardinalityOneQuery.as(baseAlias);
898
867
  let qb = db.selectFrom(aliasedCardinalityOneQuery);
899
868
  qb = applyHoistedSelections(qb, cardinalityOneQuery, baseAlias);
900
869
  for (const [key, collection] of joinCollections) if (!this.#isCollectionCardinalityOne(collection)) qb = this.#addCollectionAsJoin(qb, key, collection);
901
- if (!(isNested || isLocalSubquery)) qb = this.#applyOrderBy(qb);
870
+ if (!(isNested || isLocalSubquery)) qb = this.#applyOrderBy(qb, true);
902
871
  for (const modifier of this.#props.frontModifiers) qb = qb.modifyFront(modifier);
903
872
  for (const modifier of this.#props.endModifiers) qb = qb.modifyEnd(modifier);
904
873
  return qb;
@@ -1114,10 +1083,12 @@ var QuerySetImpl = class QuerySetImpl {
1114
1083
  delete(iqb) {
1115
1084
  return this.#asWrite(iqb);
1116
1085
  }
1117
- write(sqb) {
1086
+ write(cteFn, selectFn) {
1087
+ const qc = cteFn(this.#props.db);
1088
+ const baseQuery = selectFn(qc).withPlugin(stripWithPlugin);
1118
1089
  return this.#clone({
1119
- baseQuery: typeof sqb === "function" ? sqb(this.#props.db) : sqb,
1120
- hoistCTEs: true
1090
+ baseQuery,
1091
+ writeQueryCreator: qc
1121
1092
  });
1122
1093
  }
1123
1094
  };
@@ -1147,7 +1118,7 @@ var QuerySetCreator = class {
1147
1118
  orderByKeys: true,
1148
1119
  frontModifiers: [],
1149
1120
  endModifiers: [],
1150
- hoistCTEs: false
1121
+ writeQueryCreator: null
1151
1122
  });
1152
1123
  }
1153
1124
  selectAs(alias, query, keyBy = DEFAULT_KEY_BY) {
@@ -1162,13 +1133,14 @@ var QuerySetCreator = class {
1162
1133
  deleteAs(alias, query, keyBy = DEFAULT_KEY_BY) {
1163
1134
  return this.#createQuerySet(alias, query, keyBy);
1164
1135
  }
1165
- writeAs(alias, query, keyBy = DEFAULT_KEY_BY) {
1166
- const baseQuery = typeof query === "function" ? query(this.#db) : query;
1136
+ writeAs(alias, cteFn, selectFn, keyBy) {
1137
+ const qc = cteFn(this.#db);
1138
+ const baseQuery = selectFn(qc).withPlugin(stripWithPlugin);
1167
1139
  return new QuerySetImpl({
1168
1140
  db: this.#db,
1169
1141
  baseAlias: alias,
1170
1142
  baseQuery,
1171
- keyBy,
1143
+ keyBy: keyBy ?? DEFAULT_KEY_BY,
1172
1144
  hydrator: createHydrator().orderByKeys(),
1173
1145
  joinCollections: /* @__PURE__ */ new Map(),
1174
1146
  attachCollections: /* @__PURE__ */ new Map(),
@@ -1178,7 +1150,7 @@ var QuerySetCreator = class {
1178
1150
  orderByKeys: true,
1179
1151
  frontModifiers: [],
1180
1152
  endModifiers: [],
1181
- hoistCTEs: true
1153
+ writeQueryCreator: qc
1182
1154
  });
1183
1155
  }
1184
1156
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kysely-hydrate",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Explicit ORM-style queries with Kysely",
5
5
  "homepage": "https://github.com/GiacoCorsiglia/kysely-hydrate#readme",
6
6
  "bugs": {