@classytic/mongokit 3.10.3 → 3.11.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.
@@ -1,7 +1,7 @@
1
1
  import { n as debug, r as warn } from "./logger-DJ7egMnd.mjs";
2
- import { _ as upsert, a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, y as distinct } from "./update-CWXfFC6O.mjs";
3
- import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-CzYwrfUP.mjs";
4
- import { t as PaginationEngine } from "./PaginationEngine-B93fLCcO.mjs";
2
+ import { _ as upsert, a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, y as distinct } from "./update-DEJYcgfV.mjs";
3
+ import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-B00pI7dV.mjs";
4
+ import { t as PaginationEngine } from "./PaginationEngine-CvDFfVXT.mjs";
5
5
  import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
6
6
  import { r as isValidIdForType, t as getSchemaIdType } from "./id-resolution-e4BT9zAV.mjs";
7
7
  import { a as byIdKey, c as listQueryKey, l as modelPattern, o as byQueryKey, r as getFieldsForUser, u as versionKey } from "./field-selection-dI0m4RBs.mjs";
@@ -9,6 +9,7 @@ import mongoose from "mongoose";
9
9
  import { isFilter } from "@classytic/repo-core/filter";
10
10
  import { HOOK_PRIORITY } from "@classytic/repo-core/hooks";
11
11
  import { RepositoryBase, validatePluginOrder } from "@classytic/repo-core/repository";
12
+ import { compileUpdateSpecToMongo, isUpdateSpec } from "@classytic/repo-core/update";
12
13
  import { AsyncLocalStorage } from "node:async_hooks";
13
14
  //#region src/operations.ts
14
15
  /**
@@ -654,7 +655,7 @@ var AuditTrailQuery = class {
654
655
  //#endregion
655
656
  //#region src/plugins/batch-operations.plugin.ts
656
657
  /**
657
- * Batch operations plugin
658
+ * Batch operations plugin.
658
659
  *
659
660
  * @example
660
661
  * const repo = new Repository(Model, [
@@ -662,8 +663,12 @@ var AuditTrailQuery = class {
662
663
  * batchOperationsPlugin(),
663
664
  * ]);
664
665
  *
666
+ * // updateMany + deleteMany are primitives on Repository — no plugin needed.
665
667
  * await repo.updateMany({ status: 'pending' }, { status: 'active' });
666
668
  * await repo.deleteMany({ status: 'deleted' });
669
+ *
670
+ * // bulkWrite is what this plugin contributes.
671
+ * await repo.bulkWrite([...ops]);
667
672
  */
668
673
  function batchOperationsPlugin() {
669
674
  return {
@@ -671,38 +676,6 @@ function batchOperationsPlugin() {
671
676
  apply(repo) {
672
677
  if (!repo.registerMethod) throw new Error("batchOperationsPlugin requires methodRegistryPlugin");
673
678
  /**
674
- * Update multiple documents
675
- */
676
- repo.registerMethod("updateMany", async function(query, data, options = {}) {
677
- const context = await this._buildContext("updateMany", {
678
- query,
679
- data,
680
- ...options
681
- });
682
- try {
683
- const finalQuery = context.query || query;
684
- if (!finalQuery || Object.keys(finalQuery).length === 0) throw createError(400, "updateMany requires a non-empty query filter. Pass an explicit filter to prevent accidental mass updates.");
685
- if (Array.isArray(data) && options.updatePipeline !== true) throw createError(400, "Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates.");
686
- const finalData = context.data || data;
687
- const result = await this.Model.updateMany(finalQuery, finalData, {
688
- runValidators: true,
689
- session: options.session,
690
- ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
691
- }).exec();
692
- await this.emitAsync("after:updateMany", {
693
- context,
694
- result
695
- });
696
- return result;
697
- } catch (error) {
698
- this.emit("error:updateMany", {
699
- context,
700
- error
701
- });
702
- throw this._handleError(error);
703
- }
704
- });
705
- /**
706
679
  * Execute heterogeneous bulk write operations in a single database call.
707
680
  *
708
681
  * Supports insertOne, updateOne, updateMany, deleteOne, deleteMany, and replaceOne
@@ -751,53 +724,6 @@ function batchOperationsPlugin() {
751
724
  throw this._handleError(error);
752
725
  }
753
726
  });
754
- /**
755
- * Delete multiple documents matching a query.
756
- *
757
- * Behavior mirrors `Repository.delete`: defaults to soft-delete when
758
- * `softDeletePlugin` is wired, physical delete otherwise. Pass
759
- * `{ mode: 'hard' }` to bypass soft-delete for GDPR / cleanup paths —
760
- * multi-tenant scoping and audit hooks still fire.
761
- *
762
- * Rejects empty filters even after policy hooks run, to prevent
763
- * accidental collection wipes.
764
- */
765
- repo.registerMethod("deleteMany", async function(query, options = {}) {
766
- if (!query || Object.keys(query).length === 0) throw createError(400, "deleteMany requires a non-empty query filter. Pass an explicit filter to prevent accidental mass deletes.");
767
- const mode = options.mode;
768
- const context = await this._buildContext("deleteMany", {
769
- query,
770
- ...options,
771
- ...mode ? { deleteMode: mode } : {}
772
- });
773
- try {
774
- if (context.softDeleted) {
775
- const result = {
776
- acknowledged: true,
777
- deletedCount: 0
778
- };
779
- await this.emitAsync("after:deleteMany", {
780
- context,
781
- result
782
- });
783
- return result;
784
- }
785
- const finalQuery = context.query || query;
786
- if (!finalQuery || Object.keys(finalQuery).length === 0) throw createError(400, "deleteMany requires a non-empty query filter after policy hooks.");
787
- const result = await this.Model.deleteMany(finalQuery, { session: options.session }).exec();
788
- await this.emitAsync("after:deleteMany", {
789
- context,
790
- result
791
- });
792
- return result;
793
- } catch (error) {
794
- this.emit("error:deleteMany", {
795
- context,
796
- error
797
- });
798
- throw this._handleError(error);
799
- }
800
- });
801
727
  }
802
728
  };
803
729
  }
@@ -1817,22 +1743,25 @@ var Repository = class extends RepositoryBase {
1817
1743
  }
1818
1744
  }
1819
1745
  /**
1820
- * Create single document
1746
+ * Run a Repository operation under the standard envelope:
1747
+ * - invoke `fn`, emit `after:<op>` with `{ context, result }` on success
1748
+ * - emit `error:<op>` and rethrow via `_handleError` on failure
1749
+ *
1750
+ * Methods with branched in-try logic that emit `after:*` from multiple
1751
+ * paths (`getById`, `update`, `delete`, `deleteMany`, `aggregatePaginate`,
1752
+ * `lookupPopulate`) intentionally keep their inline try/catch rather than
1753
+ * being restructured to return a single result through this helper.
1821
1754
  */
1822
- async create(data, options = {}) {
1823
- const context = await this._buildContext("create", {
1824
- data,
1825
- ...options
1826
- });
1755
+ async _runOp(op, context, fn) {
1827
1756
  try {
1828
- const result = await create(this.Model, context.data || data, options);
1829
- await this._emitHook("after:create", {
1757
+ const result = await fn();
1758
+ await this._emitHook(`after:${op}`, {
1830
1759
  context,
1831
1760
  result
1832
1761
  });
1833
1762
  return result;
1834
1763
  } catch (error) {
1835
- await this._emitErrorHook("error:create", {
1764
+ await this._emitErrorHook(`error:${op}`, {
1836
1765
  context,
1837
1766
  error
1838
1767
  });
@@ -1840,6 +1769,16 @@ var Repository = class extends RepositoryBase {
1840
1769
  }
1841
1770
  }
1842
1771
  /**
1772
+ * Create single document
1773
+ */
1774
+ async create(data, options = {}) {
1775
+ const context = await this._buildContext("create", {
1776
+ data,
1777
+ ...options
1778
+ });
1779
+ return this._runOp("create", context, () => create(this.Model, context.data || data, options));
1780
+ }
1781
+ /**
1843
1782
  * Create multiple documents
1844
1783
  */
1845
1784
  async createMany(dataArray, options = {}) {
@@ -1847,20 +1786,7 @@ var Repository = class extends RepositoryBase {
1847
1786
  dataArray,
1848
1787
  ...options
1849
1788
  });
1850
- try {
1851
- const result = await createMany(this.Model, context.dataArray || dataArray, options);
1852
- await this._emitHook("after:createMany", {
1853
- context,
1854
- result
1855
- });
1856
- return result;
1857
- } catch (error) {
1858
- await this._emitErrorHook("error:createMany", {
1859
- context,
1860
- error
1861
- });
1862
- throw this._handleError(error);
1863
- }
1789
+ return this._runOp("createMany", context, () => createMany(this.Model, context.dataArray || dataArray, options));
1864
1790
  }
1865
1791
  /**
1866
1792
  * Get document by ID
@@ -1934,20 +1860,7 @@ var Repository = class extends RepositoryBase {
1934
1860
  return cachedResult;
1935
1861
  }
1936
1862
  const finalQuery = context.query || query;
1937
- try {
1938
- const result = await getByQuery(this.Model, finalQuery, context);
1939
- await this._emitHook("after:getByQuery", {
1940
- context,
1941
- result
1942
- });
1943
- return result;
1944
- } catch (error) {
1945
- await this._emitErrorHook("error:getByQuery", {
1946
- context,
1947
- error
1948
- });
1949
- throw this._handleError(error);
1950
- }
1863
+ return this._runOp("getByQuery", context, () => getByQuery(this.Model, finalQuery, context));
1951
1864
  }
1952
1865
  /**
1953
1866
  * Get single document by arbitrary filter.
@@ -1974,20 +1887,7 @@ var Repository = class extends RepositoryBase {
1974
1887
  return cachedResult;
1975
1888
  }
1976
1889
  const finalQuery = context.query || query;
1977
- try {
1978
- const result = await getByQuery(this.Model, finalQuery, context);
1979
- await this._emitHook("after:getOne", {
1980
- context,
1981
- result
1982
- });
1983
- return result;
1984
- } catch (error) {
1985
- await this._emitErrorHook("error:getOne", {
1986
- context,
1987
- error
1988
- });
1989
- throw this._handleError(error);
1990
- }
1890
+ return this._runOp("getOne", context, () => getByQuery(this.Model, finalQuery, context));
1991
1891
  }
1992
1892
  /**
1993
1893
  * Fetch ALL documents matching filters without pagination.
@@ -2003,7 +1903,7 @@ var Repository = class extends RepositoryBase {
2003
1903
  ...options
2004
1904
  });
2005
1905
  const resolvedFilters = context.query ?? filters;
2006
- try {
1906
+ return this._runOp("findAll", context, async () => {
2007
1907
  const query = this.Model.find(resolvedFilters);
2008
1908
  const sortSpec = context.sort || options.sort;
2009
1909
  if (sortSpec) query.sort(this._parseSort(sortSpec));
@@ -2013,19 +1913,8 @@ var Repository = class extends RepositoryBase {
2013
1913
  if (context.lean ?? options.lean ?? true) query.lean();
2014
1914
  if (options.session) query.session(options.session);
2015
1915
  if (options.readPreference) query.read(options.readPreference);
2016
- const result = await query.exec();
2017
- await this._emitHook("after:findAll", {
2018
- context,
2019
- result
2020
- });
2021
- return result;
2022
- } catch (error) {
2023
- await this._emitErrorHook("error:findAll", {
2024
- context,
2025
- error
2026
- });
2027
- throw this._handleError(error);
2028
- }
1916
+ return await query.exec();
1917
+ });
2029
1918
  }
2030
1919
  /**
2031
1920
  * Unified pagination - auto-detects offset vs keyset based on params
@@ -2098,8 +1987,8 @@ var Repository = class extends RepositoryBase {
2098
1987
  collation: context.collation ?? params.collation
2099
1988
  };
2100
1989
  const lookups = context.lookups ?? params.lookups;
2101
- if (lookups && lookups.length > 0) try {
2102
- const result = await this.lookupPopulate({
1990
+ if (lookups && lookups.length > 0) return this._runOp("getAll", context, async () => {
1991
+ return await this.lookupPopulate({
2103
1992
  filters: query,
2104
1993
  lookups,
2105
1994
  sort: paginationOptions.sort,
@@ -2112,19 +2001,8 @@ var Repository = class extends RepositoryBase {
2112
2001
  collation: paginationOptions.collation,
2113
2002
  countStrategy: context.countStrategy ?? params.countStrategy
2114
2003
  });
2115
- await this._emitHook("after:getAll", {
2116
- context,
2117
- result
2118
- });
2119
- return result;
2120
- } catch (error) {
2121
- await this._emitErrorHook("error:getAll", {
2122
- context,
2123
- error
2124
- });
2125
- throw this._handleError(error);
2126
- }
2127
- try {
2004
+ });
2005
+ return this._runOp("getAll", context, async () => {
2128
2006
  let result;
2129
2007
  if (useKeyset) result = await this._pagination.stream({
2130
2008
  ...paginationOptions,
@@ -2146,18 +2024,8 @@ var Repository = class extends RepositoryBase {
2146
2024
  countStrategy: context.countStrategy ?? params.countStrategy ?? forcedCountStrategy
2147
2025
  });
2148
2026
  }
2149
- await this._emitHook("after:getAll", {
2150
- context,
2151
- result
2152
- });
2153
2027
  return result;
2154
- } catch (error) {
2155
- await this._emitErrorHook("error:getAll", {
2156
- context,
2157
- error
2158
- });
2159
- throw this._handleError(error);
2160
- }
2028
+ });
2161
2029
  }
2162
2030
  /**
2163
2031
  * Get or create document
@@ -2169,22 +2037,11 @@ var Repository = class extends RepositoryBase {
2169
2037
  data: createData,
2170
2038
  ...options
2171
2039
  });
2172
- try {
2040
+ return this._runOp("getOrCreate", context, () => {
2173
2041
  const finalQuery = context.query || query;
2174
2042
  const finalData = context.data || createData;
2175
- const result = await getOrCreate(this.Model, finalQuery, finalData, options);
2176
- await this._emitHook("after:getOrCreate", {
2177
- context,
2178
- result
2179
- });
2180
- return result;
2181
- } catch (error) {
2182
- await this._emitErrorHook("error:getOrCreate", {
2183
- context,
2184
- error
2185
- });
2186
- throw this._handleError(error);
2187
- }
2043
+ return getOrCreate(this.Model, finalQuery, finalData, options);
2044
+ });
2188
2045
  }
2189
2046
  /**
2190
2047
  * Count documents
@@ -2195,21 +2052,7 @@ var Repository = class extends RepositoryBase {
2195
2052
  query,
2196
2053
  ...options
2197
2054
  });
2198
- try {
2199
- const finalQuery = context.query || query;
2200
- const result = await count(this.Model, finalQuery, options);
2201
- await this._emitHook("after:count", {
2202
- context,
2203
- result
2204
- });
2205
- return result;
2206
- } catch (error) {
2207
- await this._emitErrorHook("error:count", {
2208
- context,
2209
- error
2210
- });
2211
- throw this._handleError(error);
2212
- }
2055
+ return this._runOp("count", context, () => count(this.Model, context.query || query, options));
2213
2056
  }
2214
2057
  /**
2215
2058
  * Check if document exists
@@ -2220,21 +2063,7 @@ var Repository = class extends RepositoryBase {
2220
2063
  query,
2221
2064
  ...options
2222
2065
  });
2223
- try {
2224
- const finalQuery = context.query || query;
2225
- const result = await exists(this.Model, finalQuery, options);
2226
- await this._emitHook("after:exists", {
2227
- context,
2228
- result
2229
- });
2230
- return result;
2231
- } catch (error) {
2232
- await this._emitErrorHook("error:exists", {
2233
- context,
2234
- error
2235
- });
2236
- throw this._handleError(error);
2237
- }
2066
+ return this._runOp("exists", context, () => exists(this.Model, context.query || query, options));
2238
2067
  }
2239
2068
  /**
2240
2069
  * Update document by ID
@@ -2308,33 +2137,23 @@ var Repository = class extends RepositoryBase {
2308
2137
  * ```
2309
2138
  */
2310
2139
  async findOneAndUpdate(filter, update, options = {}) {
2140
+ const normalizedUpdate = isUpdateSpec(update) ? compileUpdateSpecToMongo(update) : update;
2311
2141
  const context = await this._buildContext("findOneAndUpdate", {
2312
2142
  query: filter,
2313
- data: update,
2143
+ data: normalizedUpdate,
2314
2144
  ...options
2315
2145
  });
2316
- try {
2146
+ return this._runOp("findOneAndUpdate", context, async () => {
2317
2147
  const finalQuery = context.query || filter;
2318
- const finalUpdate = context.data || update;
2319
- const result = await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
2148
+ const finalUpdate = context.data || normalizedUpdate;
2149
+ return await findOneAndUpdate(this.Model, finalQuery, finalUpdate, {
2320
2150
  ...options,
2321
2151
  sort: options.sort,
2322
2152
  returnDocument: options.returnDocument ?? "after",
2323
2153
  upsert: options.upsert ?? false,
2324
2154
  session: options.session
2325
2155
  });
2326
- await this._emitHook("after:findOneAndUpdate", {
2327
- context,
2328
- result
2329
- });
2330
- return result;
2331
- } catch (error) {
2332
- await this._emitErrorHook("error:findOneAndUpdate", {
2333
- context,
2334
- error
2335
- });
2336
- throw this._handleError(error);
2337
- }
2156
+ });
2338
2157
  }
2339
2158
  /**
2340
2159
  * Delete a document by ID.
@@ -2417,6 +2236,96 @@ var Repository = class extends RepositoryBase {
2417
2236
  }
2418
2237
  }
2419
2238
  /**
2239
+ * Update every document matching the filter.
2240
+ *
2241
+ * Promoted from `batchOperationsPlugin` to a class primitive in 3.11.0 so
2242
+ * the surface matches sqlitekit's always-available `updateMany`. Consumers
2243
+ * who forget to wire `batchOperationsPlugin` no longer get a runtime
2244
+ * `TypeError: repo.updateMany is not a function` — the method is always
2245
+ * here.
2246
+ *
2247
+ * Accepts three `update` forms via `UpdateInput`:
2248
+ * - Portable `UpdateSpec` from `@classytic/repo-core/update` (compiled to
2249
+ * `$set`/`$unset`/`$inc`/`$setOnInsert` before hooks run).
2250
+ * - Raw Mongo operator record (`{ $set, $inc, ... }`) — passed through.
2251
+ * - Mongo aggregation pipeline (`[...]`) — requires `updatePipeline: true`
2252
+ * to guard against accidental pipeline updates.
2253
+ *
2254
+ * Refuses empty filters (defense-in-depth against mass-update accidents)
2255
+ * both before and after policy hooks inject tenant scope.
2256
+ */
2257
+ async updateMany(filter, update, options = {}) {
2258
+ const normalizedData = isUpdateSpec(update) ? compileUpdateSpecToMongo(update) : update;
2259
+ const context = await this._buildContext("updateMany", {
2260
+ query: filter,
2261
+ data: normalizedData,
2262
+ ...options
2263
+ });
2264
+ return this._runOp("updateMany", context, async () => {
2265
+ const finalQuery = context.query || filter;
2266
+ if (!finalQuery || Object.keys(finalQuery).length === 0) throw createError(400, "updateMany requires a non-empty query filter. Pass an explicit filter to prevent accidental mass updates.");
2267
+ if (Array.isArray(normalizedData) && options.updatePipeline !== true) throw createError(400, "Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates.");
2268
+ const finalData = context.data || normalizedData;
2269
+ return await this.Model.updateMany(finalQuery, finalData, {
2270
+ runValidators: true,
2271
+ session: options.session,
2272
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
2273
+ }).exec();
2274
+ });
2275
+ }
2276
+ /**
2277
+ * Delete every document matching the filter.
2278
+ *
2279
+ * Promoted from `batchOperationsPlugin` to a class primitive in 3.11.0 for
2280
+ * the same reason as `updateMany` — sqlitekit parity and no silent "method
2281
+ * is undefined" footgun when the batch plugin isn't wired.
2282
+ *
2283
+ * Behavior mirrors `delete()`: defaults to soft delete when
2284
+ * `softDeletePlugin` is wired, physical delete otherwise. Pass
2285
+ * `{ mode: 'hard' }` to bypass soft-delete — multi-tenant scoping, audit,
2286
+ * and cache hooks still fire.
2287
+ *
2288
+ * Rejects empty filters up front AND after policy hooks, so a policy-plugin
2289
+ * bug that zeroes out the query can't be exploited to wipe a collection.
2290
+ */
2291
+ async deleteMany(filter, options = {}) {
2292
+ if (!filter || Object.keys(filter).length === 0) throw createError(400, "deleteMany requires a non-empty query filter. Pass an explicit filter to prevent accidental mass deletes.");
2293
+ const mode = options.mode;
2294
+ const context = await this._buildContext("deleteMany", {
2295
+ query: filter,
2296
+ ...options,
2297
+ ...mode ? { deleteMode: mode } : {}
2298
+ });
2299
+ try {
2300
+ if (context.softDeleted) {
2301
+ const result = {
2302
+ acknowledged: true,
2303
+ deletedCount: 0,
2304
+ soft: true
2305
+ };
2306
+ await this._emitHook("after:deleteMany", {
2307
+ context,
2308
+ result
2309
+ });
2310
+ return result;
2311
+ }
2312
+ const finalQuery = context.query || filter;
2313
+ if (!finalQuery || Object.keys(finalQuery).length === 0) throw createError(400, "deleteMany requires a non-empty query filter after policy hooks.");
2314
+ const result = await this.Model.deleteMany(finalQuery, { session: options.session }).exec();
2315
+ await this._emitHook("after:deleteMany", {
2316
+ context,
2317
+ result
2318
+ });
2319
+ return result;
2320
+ } catch (error) {
2321
+ await this._emitErrorHook("error:deleteMany", {
2322
+ context,
2323
+ error
2324
+ });
2325
+ throw this._handleError(error);
2326
+ }
2327
+ }
2328
+ /**
2420
2329
  * Kit-native aggregation pipeline — takes a MongoDB stage array and
2421
2330
  * returns the raw pipeline output. Use this for `$lookup`, `$unwind`,
2422
2331
  * `$facet`, `$graphLookup`, and any other mongo-specific power
@@ -2440,7 +2349,7 @@ var Repository = class extends RepositoryBase {
2440
2349
  });
2441
2350
  const maxStages = options.maxPipelineStages;
2442
2351
  if (maxStages && pipeline.length > maxStages) throw createError(400, `Aggregation pipeline exceeds maximum allowed stages (${pipeline.length} > ${maxStages})`);
2443
- try {
2352
+ return this._runOp("aggregatePipeline", context, async () => {
2444
2353
  const finalPipeline = [...pipeline];
2445
2354
  if (context.query && Object.keys(context.query).length > 0) finalPipeline.unshift({ $match: context.query });
2446
2355
  const aggregation = this.Model.aggregate(finalPipeline);
@@ -2451,19 +2360,8 @@ var Repository = class extends RepositoryBase {
2451
2360
  if (options.comment) aggregation.option({ comment: options.comment });
2452
2361
  if (options.readConcern) aggregation.option({ readConcern: options.readConcern });
2453
2362
  if (options.collation) aggregation.collation(options.collation);
2454
- const result = await aggregation.exec();
2455
- await this._emitHook("after:aggregatePipeline", {
2456
- context,
2457
- result
2458
- });
2459
- return result;
2460
- } catch (error) {
2461
- await this._emitErrorHook("error:aggregatePipeline", {
2462
- context,
2463
- error
2464
- });
2465
- throw this._handleError(error);
2466
- }
2363
+ return await aggregation.exec();
2364
+ });
2467
2365
  }
2468
2366
  /**
2469
2367
  * Paginated kit-native aggregation pipeline. Same mongo-specific
@@ -2485,20 +2383,7 @@ var Repository = class extends RepositoryBase {
2485
2383
  ...context,
2486
2384
  pipeline: finalPipeline
2487
2385
  };
2488
- try {
2489
- const result = await this._pagination.aggregatePaginate(aggOptions);
2490
- await this._emitHook("after:aggregatePipelinePaginate", {
2491
- context,
2492
- result
2493
- });
2494
- return result;
2495
- } catch (error) {
2496
- await this._emitErrorHook("error:aggregatePipelinePaginate", {
2497
- context,
2498
- error
2499
- });
2500
- throw this._handleError(error);
2501
- }
2386
+ return this._runOp("aggregatePipelinePaginate", context, () => this._pagination.aggregatePaginate(aggOptions));
2502
2387
  }
2503
2388
  /**
2504
2389
  * Portable aggregation. Compiles the repo-core `AggRequest` IR to
@@ -2524,21 +2409,10 @@ var Repository = class extends RepositoryBase {
2524
2409
  */
2525
2410
  async aggregate(req) {
2526
2411
  const context = await this._buildContext("aggregate", { aggRequest: req });
2527
- try {
2412
+ return this._runOp("aggregate", context, async () => {
2528
2413
  const finalReq = this._injectPolicyScopeIntoAgg(req, context);
2529
- const result = { rows: await executeAgg(this.Model, finalReq, { session: context.session }) };
2530
- await this._emitHook("after:aggregate", {
2531
- context,
2532
- result
2533
- });
2534
- return result;
2535
- } catch (error) {
2536
- await this._emitErrorHook("error:aggregate", {
2537
- context,
2538
- error
2539
- });
2540
- throw this._handleError(error);
2541
- }
2414
+ return { rows: await executeAgg(this.Model, finalReq, { session: context.session }) };
2415
+ });
2542
2416
  }
2543
2417
  /**
2544
2418
  * Offset-paginated portable aggregation. Same IR as `aggregate`,
@@ -2642,25 +2516,14 @@ var Repository = class extends RepositoryBase {
2642
2516
  query,
2643
2517
  ...options
2644
2518
  });
2645
- try {
2519
+ return this._runOp("distinct", context, () => {
2646
2520
  const finalQuery = context.query || query;
2647
2521
  const readPreference = context.readPreference ?? options.readPreference;
2648
- const result = await distinct(this.Model, field, finalQuery, {
2522
+ return distinct(this.Model, field, finalQuery, {
2649
2523
  session: options.session,
2650
2524
  readPreference
2651
2525
  });
2652
- await this._emitHook("after:distinct", {
2653
- context,
2654
- result
2655
- });
2656
- return result;
2657
- } catch (error) {
2658
- await this._emitErrorHook("error:distinct", {
2659
- context,
2660
- error
2661
- });
2662
- throw this._handleError(error);
2663
- }
2526
+ });
2664
2527
  }
2665
2528
  /**
2666
2529
  * Query with custom field lookups ($lookup)
@@ -2941,20 +2804,7 @@ var Repository = class extends RepositoryBase {
2941
2804
  async _executeQuery(buildQuery) {
2942
2805
  const operation = buildQuery.name || "custom";
2943
2806
  const context = await this._buildContext(operation, {});
2944
- try {
2945
- const result = await buildQuery(this.Model);
2946
- await this._emitHook(`after:${operation}`, {
2947
- context,
2948
- result
2949
- });
2950
- return result;
2951
- } catch (error) {
2952
- await this._emitErrorHook(`error:${operation}`, {
2953
- context,
2954
- error
2955
- });
2956
- throw this._handleError(error);
2957
- }
2807
+ return this._runOp(operation, context, () => buildQuery(this.Model));
2958
2808
  }
2959
2809
  /**
2960
2810
  * Build operation context and run before-hooks (sorted by priority).
@@ -3042,7 +2892,22 @@ var Repository = class extends RepositoryBase {
3042
2892
  ];
3043
2893
  } else query.$or = orConds;
3044
2894
  } else if (this._hasTextIndex) query.$text = { $search: search };
3045
- else throw createError(400, `No text index found for ${this.model}. Cannot perform text search. Configure Repository with searchMode: 'regex' (and searchFields) or 'auto' to enable index-free search.`);
2895
+ else {
2896
+ const modelName = String(this.model);
2897
+ throw createError(400, `No text index found for ${modelName}. Cannot perform text search. Configure ${modelName}'s Repository with { searchMode: 'regex', searchFields: [...] } to enable index-free search, or create a text index on the collection for searchMode: 'text'. See https://github.com/classytic/mongokit/blob/main/docs/README.md#queryparser-url--filter`, {
2898
+ code: "SEARCH_NOT_CONFIGURED",
2899
+ meta: {
2900
+ model: modelName,
2901
+ configuredMode: this.searchMode,
2902
+ availableModes: [
2903
+ "text",
2904
+ "regex",
2905
+ "auto"
2906
+ ],
2907
+ docs: "https://github.com/classytic/mongokit/blob/main/docs/README.md#queryparser-url--filter"
2908
+ }
2909
+ });
2910
+ }
3046
2911
  return query;
3047
2912
  }
3048
2913
  /**
@@ -3787,7 +3652,7 @@ function cascadePlugin(options) {
3787
3652
  */
3788
3653
  async function cascadeViaRepoBulk(relation, parentId, ctx) {
3789
3654
  const targetRepo = relation.repo;
3790
- if (typeof targetRepo.deleteMany !== "function") throw new Error(`cascadePlugin: target repo for '${ctx.targetModelName}' is missing deleteMany(). Wire batchOperationsPlugin() + methodRegistryPlugin() on the target repo.`);
3655
+ if (typeof targetRepo.deleteMany !== "function") throw new Error(`cascadePlugin: target repo for '${ctx.targetModelName}' is missing deleteMany(). Ensure the target is a mongokit Repository or implements the StandardRepo deleteMany contract.`);
3791
3656
  await targetRepo.deleteMany({ [relation.foreignKey]: parentId }, {
3792
3657
  ...ctx.scopeForward,
3793
3658
  mode: ctx.shouldSoftDelete ? "soft" : "hard",
@@ -3799,7 +3664,7 @@ async function cascadeViaRepoBulk(relation, parentId, ctx) {
3799
3664
  */
3800
3665
  async function cascadeViaRepoBulkMany(relation, parentIds, ctx) {
3801
3666
  const targetRepo = relation.repo;
3802
- if (typeof targetRepo.deleteMany !== "function") throw new Error(`cascadePlugin: target repo for '${ctx.targetModelName}' is missing deleteMany(). Wire batchOperationsPlugin() + methodRegistryPlugin() on the target repo.`);
3667
+ if (typeof targetRepo.deleteMany !== "function") throw new Error(`cascadePlugin: target repo for '${ctx.targetModelName}' is missing deleteMany(). Ensure the target is a mongokit Repository or implements the StandardRepo deleteMany contract.`);
3803
3668
  await targetRepo.deleteMany({ [relation.foreignKey]: { $in: parentIds } }, {
3804
3669
  ...ctx.scopeForward,
3805
3670
  mode: ctx.shouldSoftDelete ? "soft" : "hard",