kysely-hydrate 0.7.2 → 0.8.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 CHANGED
@@ -132,6 +132,7 @@ type Result = Array<{
132
132
  - [Inspect the SQL](#inspecting-the-sql)
133
133
  - [Mapped properties with `.mapFields()`](#mapped-properties-with-mapfields)
134
134
  - [Computed properties with `.extras()`](#computed-properties-with-extras)
135
+ - [Computed properties with `.extend()`](#computed-properties-with-extend)
135
136
  - [Excluded properties with `.omit()`](#excluded-properties-with-omit)
136
137
  - [Output transformations with `.map()`](#output-transformations-with-map)
137
138
  - [Composable mappings with `.with()`](#composable-mappings-with-with)
@@ -146,7 +147,7 @@ type Result = Array<{
146
147
  - [Output transformations with `.map()`](#output-transformations-with-map-1)
147
148
  - [Attached collections with `.attach*()`](#attached-collections-with-attach)
148
149
  - [Prefixed collections with `.has*()`](#prefixed-collections-with-has)
149
- - [Composing hydrators with `.extend()`](#composing-hydrators-with-extend)
150
+ - [Composing hydrators with `.with()`](#composing-hydrators-with-with)
150
151
  - [FAQ](#faq)
151
152
 
152
153
  ## Installation
@@ -843,6 +844,34 @@ type Result = Array<{
843
844
  }>;
844
845
  ```
845
846
 
847
+ ### Computed properties with `.extend()`
848
+
849
+ Like `.extras()`, but takes a single function that returns an object. All
850
+ returned keys are merged into the output. This is useful when multiple computed
851
+ fields share intermediate work.
852
+
853
+ ```ts
854
+ const users = await querySet(db)
855
+ .selectAs("user", db.selectFrom("users").select(["id", "firstName", "lastName", "birthDate"]))
856
+ .extend((row) => {
857
+ const names = [row.firstName, row.lastName];
858
+ return {
859
+ fullName: names.join(" "),
860
+ initials: names.map((n) => n[0]).join(""),
861
+ };
862
+ })
863
+ .execute();
864
+ // ⬇
865
+ type Result = Array<{
866
+ id: number;
867
+ firstName: string;
868
+ lastName: string;
869
+ birthDate: Date;
870
+ fullName: string;
871
+ initials: string;
872
+ }>;
873
+ ```
874
+
846
875
  ### Excluded properties with `.omit()`
847
876
 
848
877
  Remove fields from the final output. This is useful for removing intermediate
@@ -1355,7 +1384,7 @@ type Result = UserModel[];
1355
1384
 
1356
1385
  As in query sets, `.map()` is a terminal operation—after calling it, you can
1357
1386
  only call `.map()` again or `.hydrate()`. You cannot call configuration methods
1358
- like `.fields()`, `.extras()`, `.has*()`, or `.extend()`.
1387
+ like `.fields()`, `.extras()`, `.has*()`, or `.with()`.
1359
1388
 
1360
1389
  ```ts
1361
1390
  const mapped = createHydrator<User>()
@@ -1368,7 +1397,7 @@ mapped.hydrate(rows);
1368
1397
 
1369
1398
  // ❌ These don't compile:
1370
1399
  mapped.fields({ ... }); // Error: Property 'fields' does not exist
1371
- mapped.extend(...); // Error: Property 'extend' does not exist
1400
+ mapped.with(...); // Error: Property 'with' does not exist
1372
1401
  ```
1373
1402
 
1374
1403
  ### Attached collections with `.attach*()`
@@ -1423,7 +1452,7 @@ non-nullable column that was made nullable by a left join; or, (b) it's actually
1423
1452
  nullable in the "posts" table. The query set API, on the other hand, _does_
1424
1453
  know the difference, and so does not suffer from this problem.
1425
1454
 
1426
- ### Composing hydrators with `.extend()`
1455
+ ### Composing hydrators with `.with()`
1427
1456
 
1428
1457
  Merges two hydrators. The second hydrator's configuration takes precedence.
1429
1458
 
@@ -1431,7 +1460,7 @@ This is a good way to build small, reusable hydrators (for a "user preview", a
1431
1460
  "user display name", etc.) and compose them.
1432
1461
 
1433
1462
  > [!NOTE]
1434
- > Hydrators must have the same `keyBy`. If they don't, `.extend()` throws.
1463
+ > Hydrators must have the same `keyBy`. If they don't, `.with()` throws.
1435
1464
 
1436
1465
  ```ts
1437
1466
  type UserRow = { id: number; username: string; email: string };
@@ -1442,7 +1471,7 @@ const withDisplayName = createHydrator<UserRow>().extras({
1442
1471
  displayName: (u) => `${u.username} <${u.email}>`,
1443
1472
  });
1444
1473
 
1445
- const combined = base.extend(withDisplayName);
1474
+ const combined = base.with(withDisplayName);
1446
1475
  // ⬇
1447
1476
  type Result = Hydrator<UserRow, { id: number; username: string; displayName: string }>;
1448
1477
  ```
@@ -76,12 +76,12 @@ var UnsupportedNodeTypeError = class extends KyselyHydrateError {
76
76
  }
77
77
  };
78
78
  /**
79
- * Error thrown when attempting to extend a Hydrator with another Hydrator
79
+ * Error thrown when composing a Hydrator with another Hydrator
80
80
  * that has a different keyBy configuration.
81
81
  */
82
82
  var KeyByMismatchError = class extends KyselyHydrateError {
83
83
  constructor(thisKeyBy, otherKeyBy) {
84
- super(`Cannot extend hydrators with different keyBy: ${thisKeyBy} vs ${otherKeyBy}`);
84
+ super(`Cannot compose hydrators with different keyBy: ${thisKeyBy} vs ${otherKeyBy}`);
85
85
  }
86
86
  };
87
87
  /**
@@ -1,4 +1,4 @@
1
- import { d as UnsupportedAliasNodeTypeError, f as UnsupportedNodeTypeError, m as WildcardSelectionError, p as UnsupportedTableAliasNodeTypeError, t as AmbiguousColumnReferenceError, u as UnexpectedSelectionTypeError } from "./errors-EVqhxGJc.mjs";
1
+ import { d as UnsupportedAliasNodeTypeError, f as UnsupportedNodeTypeError, m as WildcardSelectionError, p as UnsupportedTableAliasNodeTypeError, t as AmbiguousColumnReferenceError, u as UnexpectedSelectionTypeError } from "./errors-CbAVHTlW.mjs";
2
2
  import * as k from "kysely";
3
3
 
4
4
  //#region src/experimental/mapped-expression.ts
package/dist/index.d.mts CHANGED
@@ -101,6 +101,15 @@ type Extras<Input> = Record<string, (input: Input) => unknown>;
101
101
  * Uses the return type of each extra function.
102
102
  */
103
103
  type InferExtras<Input, E extends Extras<Input>> = { [K in keyof E]: ReturnType<E[K]> };
104
+ /**
105
+ * An extender function that receives the full input and returns an object
106
+ * of computed properties to merge into the output.
107
+ */
108
+ type Extender<Input> = (input: Input) => Record<string, unknown>;
109
+ /**
110
+ * Infers the output type for an extender function.
111
+ */
112
+ type InferExtender<Input, F extends Extender<Input>> = ReturnType<F>;
104
113
  /**
105
114
  * The mode of a collection.
106
115
  *
@@ -298,18 +307,29 @@ interface FullHydrator<Input, Output> extends MappedHydrator<Input, Output> {
298
307
  */
299
308
  extras<E extends Extras<Input>>(extras: E): FullHydrator<Input, Extend<Output, InferExtras<Input, E>>>;
300
309
  /**
301
- * Extends this Hydrator with the configuration from another Hydrator. The
310
+ * Adds computed fields to the hydrated output by spreading the return value
311
+ * of a function. Unlike `.extras()` which defines one field at a time,
312
+ * `.extend()` calls a single function whose returned object is merged into
313
+ * the output.
314
+ *
315
+ * @param fn - A function that receives the input and returns an object of
316
+ * computed properties
317
+ * @returns A new Hydrator with the extender applied
318
+ */
319
+ extend<F extends Extender<Input>>(fn: F): FullHydrator<Input, Extend<Output, InferExtender<Input, F>>>;
320
+ /**
321
+ * Composes this Hydrator with the configuration from another Hydrator. The
302
322
  * other Hydrator's configuration takes precedence in case of conflicts.
303
323
  *
304
324
  * Both hydrators must have the same `keyBy`, and any overlapping fields
305
325
  * between the two input types must have compatible types.
306
326
  *
307
- * @param other - The Hydrator to extend with
327
+ * @param other - The Hydrator to compose with
308
328
  * @returns A new Hydrator with merged configuration
309
329
  * @throws {KeyByMismatchError} If the keyBy configurations don't match
310
330
  */
311
- extend<OtherInput extends Partial<Input>, OtherOutput>(other: FullHydrator<OtherInput, OtherOutput>): FullHydrator<Input & OtherInput, Extend<Output, OtherOutput>>;
312
- extend<OtherInput extends Partial<Input>, OtherOutput>(other: MappedHydrator<OtherInput, OtherOutput>): MappedHydrator<Input & OtherInput, Extend<Output, OtherOutput>>;
331
+ with<OtherInput extends Partial<Input>, OtherOutput>(other: FullHydrator<OtherInput, OtherOutput>): FullHydrator<Input & OtherInput, Extend<Output, OtherOutput>>;
332
+ with<OtherInput extends Partial<Input>, OtherOutput>(other: MappedHydrator<OtherInput, OtherOutput>): MappedHydrator<Input & OtherInput, Extend<Output, OtherOutput>>;
313
333
  /**
314
334
  * Configures a nested collection that exists in the same query result. The
315
335
  * child data is expected to be prefixed in the input (e.g., `posts$$id`,
@@ -621,7 +641,7 @@ interface TWithBaseQuery<in out T extends TQuerySet, in out BaseQuery extends TQ
621
641
  Collections: T["Collections"];
622
642
  JoinedQuery: TJoinedQueryWithBaseQuery<T["BaseAlias"], T["JoinedQuery"], BaseQuery>;
623
643
  OrderableColumns: T["OrderableColumns"] | (keyof BaseQuery["O"] & string);
624
- HydratedOutput: Flatten<Omit<RawExtend<BaseQuery["O"], T["HydratedOutput"]>, T["OmittedKeys"]>>;
644
+ HydratedOutput: T["IsMapped"] extends true ? T["HydratedOutput"] : Flatten<Omit<RawExtend<BaseQuery["O"], T["HydratedOutput"]>, T["OmittedKeys"]>>;
625
645
  OmittedKeys: T["OmittedKeys"];
626
646
  }
627
647
  interface TWithOutput<in out T extends TQuerySet, in out Output> {
@@ -1359,6 +1379,37 @@ interface QuerySet<in out T extends TQuerySet> extends MappedQuerySet<T> {
1359
1379
  * @returns A new HydratedQueryBuilder with the extras applied.
1360
1380
  */
1361
1381
  extras<E extends Extras<TInput<T>>>(extras: E): QuerySet<TWithExtendedOutput<T, InferExtras<TInput<T>, E>>>;
1382
+ /**
1383
+ * Adds computed fields to the hydrated output by spreading the return value
1384
+ * of a function. Unlike `.extras()` which defines one field at a time,
1385
+ * `.extend()` calls a single function whose returned object is merged into
1386
+ * the output.
1387
+ *
1388
+ * ### Examples
1389
+ *
1390
+ * ```ts
1391
+ * const users = await querySet(db)
1392
+ * .selectAs("users", (eb) => eb.selectFrom("users").select(["users.id", "users.firstName", "users.lastName"]))
1393
+ * .extend((row) => ({
1394
+ * fullName: `${row.firstName} ${row.lastName}`,
1395
+ * initials: `${row.firstName[0]}${row.lastName[0]}`,
1396
+ * }))
1397
+ * .execute();
1398
+ * // ⬇
1399
+ * type Result = Array<{
1400
+ * id: number;
1401
+ * firstName: string;
1402
+ * lastName: string;
1403
+ * fullName: string;
1404
+ * initials: string;
1405
+ * }>;
1406
+ * ```
1407
+ *
1408
+ * @param fn - A function that receives the row and returns an object of
1409
+ * computed properties.
1410
+ * @returns A new HydratedQueryBuilder with the extender applied.
1411
+ */
1412
+ extend<F extends Extender<TInput<T>>>(fn: F): QuerySet<TWithExtendedOutput<T, InferExtender<TInput<T>, F>>>;
1362
1413
  /**
1363
1414
  * Transforms already-selected field values in the hydrated output. Fields
1364
1415
  * not mentioned in the mappings will still be included as-is.
@@ -2581,7 +2632,7 @@ declare class UnsupportedNodeTypeError extends KyselyHydrateError {
2581
2632
  constructor(kind: string);
2582
2633
  }
2583
2634
  /**
2584
- * Error thrown when attempting to extend a Hydrator with another Hydrator
2635
+ * Error thrown when composing a Hydrator with another Hydrator
2585
2636
  * that has a different keyBy configuration.
2586
2637
  */
2587
2638
  declare class KeyByMismatchError extends KyselyHydrateError {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as KeyByMismatchError, c as UnexpectedComplexAliasError, d as UnsupportedAliasNodeTypeError, f as UnsupportedNodeTypeError, i as InvalidJoinedQuerySetError, l as UnexpectedSelectAllError, m as WildcardSelectionError, n as CardinalityViolationError, o as KyselyHydrateError, p as UnsupportedTableAliasNodeTypeError, r as ExpectedOneItemError, s as UnexpectedCaseError, t as AmbiguousColumnReferenceError, u as UnexpectedSelectionTypeError } from "./errors-EVqhxGJc.mjs";
1
+ import { a as KeyByMismatchError, c as UnexpectedComplexAliasError, d as UnsupportedAliasNodeTypeError, f as UnsupportedNodeTypeError, i as InvalidJoinedQuerySetError, l as UnexpectedSelectAllError, m as WildcardSelectionError, n as CardinalityViolationError, o as KyselyHydrateError, p as UnsupportedTableAliasNodeTypeError, r as ExpectedOneItemError, s as UnexpectedCaseError, t as AmbiguousColumnReferenceError, u as UnexpectedSelectionTypeError } from "./errors-CbAVHTlW.mjs";
2
2
  import * as k from "kysely";
3
3
 
4
4
  //#region src/helpers/order-by.ts
@@ -264,7 +264,13 @@ var HydratorImpl = class HydratorImpl {
264
264
  extras: addObjectToMap(this.#props.extras, extras)
265
265
  });
266
266
  }
267
- extend(other) {
267
+ extend(fn) {
268
+ return new HydratorImpl({
269
+ ...this.#props,
270
+ extenders: [...this.#props.extenders ?? [], fn]
271
+ });
272
+ }
273
+ with(other) {
268
274
  const otherImpl = other;
269
275
  const thisKeyBy = JSON.stringify(this.#props.keyBy);
270
276
  const otherKeyBy = JSON.stringify(otherImpl.#props.keyBy);
@@ -276,6 +282,7 @@ var HydratorImpl = class HydratorImpl {
276
282
  keyBy: otherProps.keyBy,
277
283
  fields: new Map([...ownProps.fields ?? [], ...otherProps.fields ?? []]),
278
284
  extras: new Map([...ownProps.extras ?? [], ...otherProps.extras ?? []]),
285
+ extenders: [...ownProps.extenders ?? [], ...otherProps.extenders ?? []],
279
286
  collections: mergedCollections,
280
287
  attachedCollections: new Map([...ownProps.attachedCollections ?? [], ...otherProps.attachedCollections ?? []]),
281
288
  mapFns: [...this.#props.mapFns ?? [], ...otherProps.mapFns ?? []],
@@ -412,7 +419,7 @@ var HydratorImpl = class HydratorImpl {
412
419
  * Hydrates a single entity. All attach collections are already fetched and provided in attachedDataMap.
413
420
  */
414
421
  #hydrateOne(ctx, prefix, input, inputRows) {
415
- const { fields, extras, collections, attachedCollections } = this.#props;
422
+ const { fields, extras, extenders, collections, attachedCollections } = this.#props;
416
423
  const entity = {};
417
424
  if (ctx.autoIncludeFields) for (const key of this.#getAutoFields(ctx, prefix, input)) entity[key] = getPrefixedValue(prefix, input, key);
418
425
  if (fields) for (const [key, field] of fields) {
@@ -420,9 +427,10 @@ var HydratorImpl = class HydratorImpl {
420
427
  const value = getPrefixedValue(prefix, input, key);
421
428
  entity[key] = field === true ? value : field(value);
422
429
  }
423
- if (extras) {
430
+ if (extras || extenders) {
424
431
  const accessor = createdPrefixedAccessor(prefix, input);
425
- for (const [key, extra] of extras) entity[key] = extra(accessor);
432
+ if (extras) for (const [key, extra] of extras) entity[key] = extra(accessor);
433
+ if (extenders) for (const extender of extenders) Object.assign(entity, extender(accessor));
426
434
  }
427
435
  if (collections) {
428
436
  const childCtx = this.#props.hasMultipleManyCollections && !ctx.hasSiblingManyCollections ? {
@@ -878,6 +886,9 @@ var QuerySetImpl = class QuerySetImpl {
878
886
  extras(extras) {
879
887
  return this.#clone({ hydrator: asFullHydrator(this.#props.hydrator).extras(extras) });
880
888
  }
889
+ extend(fn) {
890
+ return this.#clone({ hydrator: asFullHydrator(this.#props.hydrator).extend(fn) });
891
+ }
881
892
  mapFields(mappings) {
882
893
  return this.#clone({ hydrator: asFullHydrator(this.#props.hydrator).fields(mappings) });
883
894
  }
@@ -885,7 +896,7 @@ var QuerySetImpl = class QuerySetImpl {
885
896
  return this.#clone({ hydrator: asFullHydrator(this.#props.hydrator).omit(keys) });
886
897
  }
887
898
  with(hydrator) {
888
- return this.#clone({ hydrator: asFullHydrator(this.#props.hydrator).extend(hydrator) });
899
+ return this.#clone({ hydrator: asFullHydrator(this.#props.hydrator).with(hydrator) });
889
900
  }
890
901
  #addAttach(mode, key, fetchFn, keys) {
891
902
  return this.#clone({ hydrator: asFullHydrator(this.#props.hydrator).attach(mode, key, fetchFn, keys) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kysely-hydrate",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Explicit ORM-style queries with Kysely",
5
5
  "homepage": "https://github.com/GiacoCorsiglia/kysely-hydrate#readme",
6
6
  "bugs": {