kysely-hydrate 0.4.0 → 0.4.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/dist/index.d.mts CHANGED
@@ -127,20 +127,6 @@ type HydratorArg<Input, Output> = MappedHydrator<Input, Output> | HydratorFactor
127
127
  * The input type is automatically prefixed based on the parent's prefix.
128
128
  */
129
129
  type ChildHydratorArg<P extends string, ParentInput, ChildOutput> = MappedHydrator<SelectAndStripPrefix<P, ParentInput>, ChildOutput> | ((create: CreateHydratorFn<SelectAndStripPrefix<P, ParentInput>>) => MappedHydrator<SelectAndStripPrefix<P, ParentInput>, ChildOutput>);
130
- /**
131
- * Ensures fields are included in the Hydrator's output if they don't already
132
- * have an explicit configuration (including explicit omission).
133
- *
134
- * This is used internally by `HydratedQueryBuilder` to auto-detect selected
135
- * fields without overwriting explicit field mappings or omissions from
136
- * `mapFields()` and `omit()`.
137
- *
138
- * @param hydrator - The Hydrator instance
139
- * @param fieldNames - Field names to ensure are included
140
- * @returns A new Hydrator with the fields ensured
141
- * @internal
142
- */
143
-
144
130
  declare const IsFullHydrator: unique symbol;
145
131
  /**
146
132
  * A configuration for how to hydrate flat database rows into a denormalized structure.
@@ -1042,13 +1028,6 @@ declare class UnsupportedTableAliasNodeTypeError extends KyselyHydrateError {
1042
1028
  declare class UnsupportedNodeTypeError extends KyselyHydrateError {
1043
1029
  constructor(kind: string);
1044
1030
  }
1045
- /**
1046
- * Error thrown when attempting to call a private method on an instance that
1047
- * was not properly registered with the private accessor.
1048
- */
1049
- declare class InvalidInstanceError extends KyselyHydrateError {
1050
- constructor();
1051
- }
1052
1031
  /**
1053
1032
  * Error thrown when attempting to extend a Hydrator with another Hydrator
1054
1033
  * that has a different keyBy configuration.
@@ -1057,4 +1036,4 @@ declare class KeyByMismatchError extends KyselyHydrateError {
1057
1036
  constructor(thisKeyBy: string, otherKeyBy: string);
1058
1037
  }
1059
1038
  //#endregion
1060
- export { AmbiguousColumnReferenceError, ExpectedOneItemError, HydratePlugin, type Hydrator, InvalidInstanceError, KeyByMismatchError, KyselyHydrateError, UnexpectedCaseError, UnexpectedComplexAliasError, UnexpectedSelectAllError, UnexpectedSelectionTypeError, UnsupportedAliasNodeTypeError, UnsupportedNodeTypeError, UnsupportedTableAliasNodeTypeError, WildcardSelectionError, createHydrator, hydrate, hydrateData, isFullHydrator, map };
1039
+ export { AmbiguousColumnReferenceError, ExpectedOneItemError, HydratePlugin, type Hydrator, KeyByMismatchError, KyselyHydrateError, UnexpectedCaseError, UnexpectedComplexAliasError, UnexpectedSelectAllError, UnexpectedSelectionTypeError, UnsupportedAliasNodeTypeError, UnsupportedNodeTypeError, UnsupportedTableAliasNodeTypeError, WildcardSelectionError, createHydrator, hydrate, hydrateData, isFullHydrator, map };
package/dist/index.mjs CHANGED
@@ -70,15 +70,6 @@ var UnsupportedNodeTypeError = class extends KyselyHydrateError {
70
70
  }
71
71
  };
72
72
  /**
73
- * Error thrown when attempting to call a private method on an instance that
74
- * was not properly registered with the private accessor.
75
- */
76
- var InvalidInstanceError = class extends KyselyHydrateError {
77
- constructor() {
78
- super("Invalid instance - private method not registered");
79
- }
80
- };
81
- /**
82
73
  * Error thrown when attempting to extend a Hydrator with another Hydrator
83
74
  * that has a different keyBy configuration.
84
75
  */
@@ -103,12 +94,6 @@ function makePrefix(prefix, key) {
103
94
  return `${prefix}${key}${SEP}`;
104
95
  }
105
96
  /**
106
- * Indicates whether a string has any prefixes from `makePrefix`applied to it.
107
- */
108
- function hasAnyPrefix(string) {
109
- return string.includes(SEP);
110
- }
111
- /**
112
97
  * Applies a prefix to a key.
113
98
  *
114
99
  * ```ts
@@ -171,49 +156,6 @@ function addObjectToMap(map$1, obj) {
171
156
  }
172
157
  return clone;
173
158
  }
174
- /**
175
- * Creates a pair of functions for accessing private instance methods from outside the class.
176
- * Uses a WeakMap to store bound methods, ensuring proper memory cleanup.
177
- *
178
- * @returns An object with `call` and `register` functions
179
- * - call: Calls the private method on an instance
180
- * - register: Registers a private method for an instance (call in constructor)
181
- *
182
- * @example
183
- * ```ts
184
- * const privateAccessor = createPrivateAccessor<
185
- * MyClass,
186
- * [arg1: string, arg2: number],
187
- * ReturnType
188
- * >();
189
- *
190
- * class MyClass {
191
- * constructor() {
192
- * privateAccessor.register(this, this.#privateMethod.bind(this));
193
- * }
194
- * #privateMethod(arg1: string, arg2: number): ReturnType { ... }
195
- * }
196
- *
197
- * // External usage with proper JSDoc:
198
- * \/**
199
- * * Documentation here
200
- * *\/
201
- * export const callPrivateMethod = privateAccessor.call;
202
- * ```
203
- */
204
- function createPrivateAccessor() {
205
- const registry = /* @__PURE__ */ new WeakMap();
206
- return {
207
- register: (instance, method) => {
208
- registry.set(instance, method);
209
- },
210
- call: (instance, ...args) => {
211
- const method = registry.get(instance);
212
- if (!method) throw new InvalidInstanceError();
213
- return method(...args);
214
- }
215
- };
216
- }
217
159
 
218
160
  //#endregion
219
161
  //#region src/hydrator.ts
@@ -222,24 +164,6 @@ function createPrivateAccessor() {
222
164
  * Only used when the input type has an "id" property.
223
165
  */
224
166
  const DEFAULT_KEY_BY = "id";
225
- /**
226
- * Private accessor for the ensureFields method.
227
- */
228
- const ensureFieldsAccessor = createPrivateAccessor();
229
- /**
230
- * Ensures fields are included in the Hydrator's output if they don't already
231
- * have an explicit configuration (including explicit omission).
232
- *
233
- * This is used internally by `HydratedQueryBuilder` to auto-detect selected
234
- * fields without overwriting explicit field mappings or omissions from
235
- * `mapFields()` and `omit()`.
236
- *
237
- * @param hydrator - The Hydrator instance
238
- * @param fieldNames - Field names to ensure are included
239
- * @returns A new Hydrator with the fields ensured
240
- * @internal
241
- */
242
- const ensureFields = ensureFieldsAccessor.call;
243
167
  const IsFullHydrator = Symbol("HydratorType");
244
168
  /**
245
169
  * Determines if a hydrator is a full hydrator, meaning it has not had a .map()
@@ -261,13 +185,16 @@ const asFullHydrator = (hydrator) => {
261
185
  throw new Error("Hydrator is not a full hydrator");
262
186
  };
263
187
  /**
188
+ * Special constant to enable auto-inclusion of fields at each level.
189
+ */
190
+ const EnableAutoInclusion = Symbol();
191
+ /**
264
192
  * Implements the entire inheritance chain of Hydrators.
265
193
  */
266
194
  var HydratorImpl = class HydratorImpl {
267
195
  #props;
268
196
  constructor(props) {
269
197
  this.#props = props;
270
- ensureFieldsAccessor.register(this, this.#ensureFields.bind(this));
271
198
  }
272
199
  get [IsFullHydrator]() {
273
200
  return !this.#props.mapFns?.length;
@@ -285,14 +212,6 @@ var HydratorImpl = class HydratorImpl {
285
212
  fields: addObjectToMap(this.#props.fields, omitFields)
286
213
  });
287
214
  }
288
- #ensureFields(fieldNames) {
289
- const clone = new Map(this.#props.fields);
290
- for (const name of fieldNames) if (!clone.has(name)) clone.set(name, true);
291
- return new HydratorImpl({
292
- ...this.#props,
293
- fields: clone
294
- });
295
- }
296
215
  extras(extras) {
297
216
  return new HydratorImpl({
298
217
  ...this.#props,
@@ -367,7 +286,7 @@ var HydratorImpl = class HydratorImpl {
367
286
  *
368
287
  * Writes directly to the provided attachedDataMap and fetchPromises array.
369
288
  */
370
- #fetchAllAttachedCollections(prefix, inputs, attachedDataMap, fetchPromises) {
289
+ #fetchAllAttachedCollections(ctx, prefix, inputs, fetchPromises) {
371
290
  const { attachedCollections, collections } = this.#props;
372
291
  if (attachedCollections) {
373
292
  let inputArray;
@@ -380,21 +299,45 @@ var HydratorImpl = class HydratorImpl {
380
299
  const mapKey = prefix ? applyPrefix(prefix, key) : key;
381
300
  fetchPromises.push(Promise.resolve(attachedCollection.fetchFn(inputArray)).then((attachedOutputs) => {
382
301
  const grouped = groupByKey("", attachedOutputs, attachedCollection.matchChild);
383
- attachedDataMap.set(mapKey, grouped);
302
+ ctx.attachedDataMap.set(mapKey, grouped);
384
303
  }));
385
304
  }
386
305
  }
387
306
  if (collections) for (const collection of collections.values()) {
388
307
  const childPrefix = applyPrefix(prefix, collection.prefix);
389
- collection.hydrator.#fetchAllAttachedCollections(childPrefix, inputs, attachedDataMap, fetchPromises);
308
+ collection.hydrator.#fetchAllAttachedCollections(ctx, childPrefix, inputs, fetchPromises);
309
+ }
310
+ }
311
+ /**
312
+ * Gets all the fields belonging to the current prefix level; not to the
313
+ * parent, and not to any nested collection. Does this once per hydration
314
+ * (assumes all inputs have the same keys).
315
+ */
316
+ #getAutoFields(ctx, prefix, input) {
317
+ const cached = ctx.autoFieldsCache.get(prefix);
318
+ if (cached) return cached;
319
+ if (typeof input !== "object" || input === null) return [];
320
+ const { fields, extras, collections } = this.#props;
321
+ const nestedPrefixes = [];
322
+ if (collections) for (const collection of collections.values()) nestedPrefixes.push(applyPrefix(prefix, collection.prefix));
323
+ const autoFields = [];
324
+ for (const inputKey of Object.keys(input)) {
325
+ if (!hasPrefix(prefix, inputKey)) continue;
326
+ if (nestedPrefixes.some((nestedPrefix) => hasPrefix(nestedPrefix, inputKey))) continue;
327
+ const unprefixedKey = removePrefix(prefix, inputKey);
328
+ if (fields?.has(unprefixedKey) || extras?.has(unprefixedKey)) continue;
329
+ autoFields.push(unprefixedKey);
390
330
  }
331
+ ctx.autoFieldsCache.set(prefix, autoFields);
332
+ return autoFields;
391
333
  }
392
334
  /**
393
335
  * Hydrates a single entity. All attach collections are already fetched and provided in attachedDataMap.
394
336
  */
395
- #hydrateOne(prefix, attachedDataMap, input, inputRows) {
337
+ #hydrateOne(ctx, prefix, input, inputRows) {
396
338
  const { fields, extras, collections, attachedCollections } = this.#props;
397
339
  const entity = {};
340
+ if (ctx.autoIncludeFields) for (const key of this.#getAutoFields(ctx, prefix, input)) entity[key] = getPrefixedValue(prefix, input, key);
398
341
  if (fields) for (const [key, field] of fields) {
399
342
  if (field === false) continue;
400
343
  const value = getPrefixedValue(prefix, input, key);
@@ -406,12 +349,12 @@ var HydratorImpl = class HydratorImpl {
406
349
  }
407
350
  if (collections) for (const [key, collection] of collections) {
408
351
  const childPrefix = applyPrefix(prefix, collection.prefix);
409
- entity[key] = applyCollectionMode(collection.hydrator.#hydrateMany(childPrefix, inputRows, attachedDataMap), collection.mode, key);
352
+ entity[key] = applyCollectionMode(collection.hydrator.#hydrateMany(ctx, childPrefix, inputRows), collection.mode, key);
410
353
  }
411
354
  if (attachedCollections) for (const [key, collection] of attachedCollections) {
412
355
  const inputKey = getKey(prefix, input, collection.toParent);
413
356
  const mapKey = prefix ? applyPrefix(prefix, key) : key;
414
- const attachedRows = attachedDataMap.get(mapKey)?.get(inputKey);
357
+ const attachedRows = ctx.attachedDataMap.get(mapKey)?.get(inputKey);
415
358
  entity[key] = applyCollectionMode(attachedRows, collection.mode, key);
416
359
  }
417
360
  const { mapFns } = this.#props;
@@ -425,13 +368,13 @@ var HydratorImpl = class HydratorImpl {
425
368
  /**
426
369
  * Hydrates many entities. All attach collections are already fetched and provided in attachedDataMap.
427
370
  */
428
- #hydrateMany(prefix, inputs, attachedDataMap) {
371
+ #hydrateMany(ctx, prefix, inputs) {
429
372
  const { keyBy, collections } = this.#props;
430
373
  const result = [];
431
374
  if (!collections) {
432
375
  for (const input of inputs) {
433
376
  if (isKeyNil(getKey(prefix, input, keyBy))) continue;
434
- const entity = this.#hydrateOne(prefix, attachedDataMap, input, [input]);
377
+ const entity = this.#hydrateOne(ctx, prefix, input, [input]);
435
378
  result.push(entity);
436
379
  }
437
380
  return result;
@@ -439,18 +382,22 @@ var HydratorImpl = class HydratorImpl {
439
382
  const grouped = groupByKey(prefix, inputs, keyBy);
440
383
  for (const groupRows of grouped.values()) {
441
384
  const firstRow = groupRows[0];
442
- const entity = this.#hydrateOne(prefix, attachedDataMap, firstRow, groupRows);
385
+ const entity = this.#hydrateOne(ctx, prefix, firstRow, groupRows);
443
386
  result.push(entity);
444
387
  }
445
388
  return result;
446
389
  }
447
- hydrate(input) {
448
- const attachedDataMap = /* @__PURE__ */ new Map();
390
+ hydrate(input, autoIncludeFields) {
391
+ const ctx = {
392
+ autoIncludeFields: autoIncludeFields === EnableAutoInclusion,
393
+ attachedDataMap: /* @__PURE__ */ new Map(),
394
+ autoFieldsCache: /* @__PURE__ */ new Map()
395
+ };
449
396
  const fetchPromises = [];
450
- this.#fetchAllAttachedCollections("", isIterable(input) ? input : [input], attachedDataMap, fetchPromises);
397
+ this.#fetchAllAttachedCollections(ctx, "", isIterable(input) ? input : [input], fetchPromises);
451
398
  const hydrateWithData = () => {
452
- if (isIterable(input)) return this.#hydrateMany("", input, attachedDataMap);
453
- return this.#hydrateOne("", attachedDataMap, input, [input]);
399
+ if (isIterable(input)) return this.#hydrateMany(ctx, "", input);
400
+ return this.#hydrateOne(ctx, "", input, [input]);
454
401
  };
455
402
  return fetchPromises.length > 0 ? Promise.all(fetchPromises).then(hydrateWithData) : Promise.resolve(hydrateWithData());
456
403
  }
@@ -647,23 +594,20 @@ var HydratedQueryBuilderImpl = class HydratedQueryBuilderImpl {
647
594
  return new AliasedHydratedQueryBuilder(alias, this);
648
595
  }
649
596
  #hydrate(rows) {
650
- const isArray = Array.isArray(rows);
651
- const firstRow = isArray ? rows[0] : rows;
652
- if (!firstRow) return isArray ? [] : void 0;
653
- const fieldNames = Object.keys(firstRow).filter((key) => !hasAnyPrefix(key));
654
- return ensureFields(this.#props.hydrator, fieldNames).hydrate(rows);
597
+ return this.#props.hydrator.hydrate(rows, EnableAutoInclusion);
655
598
  }
656
599
  async execute() {
657
600
  const rows = await this.#props.qb.execute();
658
601
  return this.#hydrate(rows);
659
602
  }
660
603
  async executeTakeFirst() {
661
- const result = await this.#props.qb.executeTakeFirst();
662
- return result ? this.#hydrate(result) : void 0;
604
+ const [result] = await this.execute();
605
+ return result;
663
606
  }
664
607
  async executeTakeFirstOrThrow(errorConstructor = k.NoResultError) {
665
- const result = await this.#props.qb.executeTakeFirstOrThrow(errorConstructor);
666
- return this.#hydrate(result);
608
+ const result = await this.executeTakeFirst();
609
+ if (result === void 0) throw k.isNoResultErrorConstructor(errorConstructor) ? new errorConstructor(this.toOperationNode()) : errorConstructor(this.toOperationNode());
610
+ return result;
667
611
  }
668
612
  extras(extras) {
669
613
  return new HydratedQueryBuilderImpl({
@@ -729,8 +673,7 @@ var HydratedQueryBuilderImpl = class HydratedQueryBuilderImpl {
729
673
  const prefixedSelections = prefixSelectArg(this.#props.prefix, selection);
730
674
  return new HydratedQueryBuilderImpl({
731
675
  ...this.#props,
732
- qb: this.#props.qb.select(prefixedSelections),
733
- hydrator: asFullHydrator(this.#props.hydrator).fields(prefixedSelections.map((selection$1) => selection$1.originalName))
676
+ qb: this.#props.qb.select(prefixedSelections)
734
677
  });
735
678
  }
736
679
  #join(method, from, ...args) {
@@ -748,8 +691,7 @@ var HydratedQueryBuilderImpl = class HydratedQueryBuilderImpl {
748
691
  let newQb = this.#props.qb;
749
692
  newQb = newQb[method](aliasedQb, ...args);
750
693
  newQb = newQb.select(hoistedSelections);
751
- let newHydrator = asFullHydrator(this.#props.hydrator).extend(nestedHydrator);
752
- newHydrator = ensureFields(newHydrator, hoistedSelections.map((selection) => selection.originalName).filter((name) => !hasAnyPrefix(name)));
694
+ const newHydrator = asFullHydrator(this.#props.hydrator).extend(nestedHydrator);
753
695
  return new HydratedQueryBuilderImpl({
754
696
  ...this.#props,
755
697
  qb: newQb,
@@ -1175,4 +1117,4 @@ var HydratePlugin = class {
1175
1117
  };
1176
1118
 
1177
1119
  //#endregion
1178
- export { AmbiguousColumnReferenceError, ExpectedOneItemError, HydratePlugin, InvalidInstanceError, KeyByMismatchError, KyselyHydrateError, UnexpectedCaseError, UnexpectedComplexAliasError, UnexpectedSelectAllError, UnexpectedSelectionTypeError, UnsupportedAliasNodeTypeError, UnsupportedNodeTypeError, UnsupportedTableAliasNodeTypeError, WildcardSelectionError, createHydrator, hydrate, hydrateData, isFullHydrator, map };
1120
+ export { AmbiguousColumnReferenceError, ExpectedOneItemError, HydratePlugin, KeyByMismatchError, KyselyHydrateError, UnexpectedCaseError, UnexpectedComplexAliasError, UnexpectedSelectAllError, UnexpectedSelectionTypeError, UnsupportedAliasNodeTypeError, UnsupportedNodeTypeError, UnsupportedTableAliasNodeTypeError, WildcardSelectionError, createHydrator, hydrate, hydrateData, isFullHydrator, map };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kysely-hydrate",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Explicit ORM-style queries with Kysely",
5
5
  "license": "MIT",
6
6
  "author": {