@classytic/mongokit 3.4.0 → 3.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.
@@ -1,7 +1,7 @@
1
1
  import { a as warn, i as debug, n as parseDuplicateKeyError, t as createError } from "./error-Bpbi_NKo.mjs";
2
- import { _ as LookupBuilder, a as getById, d as create, f as createMany, g as distinct, i as exists, l as deleteById, m as upsert, o as getByQuery, r as count, s as getOrCreate, t as update } from "./update-DXwVh6M1.mjs";
3
- import { t as PaginationEngine } from "./PaginationEngine-PLyDhrO7.mjs";
4
- import { a as byIdKey, c as listQueryKey, l as modelPattern, o as byQueryKey, r as getFieldsForUser, u as versionKey } from "./field-selection-CalOB7yM.mjs";
2
+ import { _ as LookupBuilder, a as getById, d as create, f as createMany, g as distinct, i as exists, l as deleteById, m as upsert, o as getByQuery, r as count, s as getOrCreate, t as update } from "./update-DGKMmBgG.mjs";
3
+ import { t as PaginationEngine } from "./PaginationEngine-nY04eGUM.mjs";
4
+ import { a as byIdKey, c as listQueryKey, l as modelPattern, o as byQueryKey, r as getFieldsForUser, u as versionKey } from "./field-selection-reyDRzXf.mjs";
5
5
  import mongoose from "mongoose";
6
6
  //#region src/plugins/aggregate-helpers.plugin.ts
7
7
  /**
@@ -30,14 +30,14 @@ function aggregateHelpersPlugin() {
30
30
  count: { $sum: 1 }
31
31
  } }, { $sort: { count: -1 } }];
32
32
  if (options.limit) pipeline.push({ $limit: options.limit });
33
- return this.aggregate.call(this, pipeline, options);
33
+ return this.aggregate(pipeline, options);
34
34
  });
35
35
  const aggregateOperation = async function(field, operator, resultKey, query = {}, options = {}) {
36
36
  const pipeline = [{ $match: query }, { $group: {
37
37
  _id: null,
38
38
  [resultKey]: { [operator]: `$${field}` }
39
39
  } }];
40
- return (await this.aggregate.call(this, pipeline, options))[0]?.[resultKey] || 0;
40
+ return (await this.aggregate(pipeline, options))[0]?.[resultKey] || 0;
41
41
  };
42
42
  /**
43
43
  * Sum field values
@@ -501,7 +501,7 @@ function batchOperationsPlugin() {
501
501
  * Update multiple documents
502
502
  */
503
503
  repo.registerMethod("updateMany", async function(query, data, options = {}) {
504
- const context = await this._buildContext.call(this, "updateMany", {
504
+ const context = await this._buildContext("updateMany", {
505
505
  query,
506
506
  data,
507
507
  ...options
@@ -526,7 +526,7 @@ function batchOperationsPlugin() {
526
526
  context,
527
527
  error
528
528
  });
529
- throw this._handleError.call(this, error);
529
+ throw this._handleError(error);
530
530
  }
531
531
  });
532
532
  /**
@@ -544,7 +544,7 @@ function batchOperationsPlugin() {
544
544
  * ]);
545
545
  */
546
546
  repo.registerMethod("bulkWrite", async function(operations, options = {}) {
547
- const context = await this._buildContext.call(this, "bulkWrite", {
547
+ const context = await this._buildContext("bulkWrite", {
548
548
  operations,
549
549
  ...options
550
550
  });
@@ -575,14 +575,14 @@ function batchOperationsPlugin() {
575
575
  context,
576
576
  error
577
577
  });
578
- throw this._handleError.call(this, error);
578
+ throw this._handleError(error);
579
579
  }
580
580
  });
581
581
  /**
582
582
  * Delete multiple documents
583
583
  */
584
584
  repo.registerMethod("deleteMany", async function(query, options = {}) {
585
- const context = await this._buildContext.call(this, "deleteMany", {
585
+ const context = await this._buildContext("deleteMany", {
586
586
  query,
587
587
  ...options
588
588
  });
@@ -611,7 +611,7 @@ function batchOperationsPlugin() {
611
611
  context,
612
612
  error
613
613
  });
614
- throw this._handleError.call(this, error);
614
+ throw this._handleError(error);
615
615
  }
616
616
  });
617
617
  }
@@ -1105,6 +1105,13 @@ var AggregationBuilder = class AggregationBuilder {
1105
1105
  };
1106
1106
  //#endregion
1107
1107
  //#region src/Repository.ts
1108
+ function ensureLookupProjectionIncludesCursorFields(projection, sort) {
1109
+ if (!projection || !sort) return projection;
1110
+ if (!Object.values(projection).some((value) => value === 1)) return projection;
1111
+ const nextProjection = { ...projection };
1112
+ for (const field of [...Object.keys(sort), "_id"]) nextProjection[field] = 1;
1113
+ return nextProjection;
1114
+ }
1108
1115
  /**
1109
1116
  * Plugin phase priorities (lower = runs first)
1110
1117
  * Policy hooks (multi-tenant, soft-delete, validation) MUST run before cache
@@ -1156,7 +1163,7 @@ var Repository = class {
1156
1163
  */
1157
1164
  on(event, listener, options) {
1158
1165
  if (!this._hooks.has(event)) this._hooks.set(event, []);
1159
- const hooks = this._hooks.get(event);
1166
+ const hooks = this._hooks.get(event) ?? [];
1160
1167
  const priority = options?.priority ?? HOOK_PRIORITY.DEFAULT;
1161
1168
  hooks.push({
1162
1169
  listener,
@@ -1413,32 +1420,49 @@ var Repository = class {
1413
1420
  session: options.session,
1414
1421
  hint: context.hint ?? params.hint,
1415
1422
  maxTimeMS: context.maxTimeMS ?? params.maxTimeMS,
1416
- readPreference: context.readPreference ?? options.readPreference ?? params.readPreference
1423
+ readPreference: context.readPreference ?? options.readPreference ?? params.readPreference,
1424
+ collation: context.collation ?? params.collation
1417
1425
  };
1418
- const lookups = params.lookups;
1426
+ const lookups = context.lookups ?? params.lookups;
1419
1427
  if (lookups && lookups.length > 0) try {
1420
1428
  const lookupResult = await this.lookupPopulate({
1421
1429
  filters: query,
1422
1430
  lookups,
1423
1431
  sort: paginationOptions.sort,
1424
- page: page || 1,
1432
+ page: useKeyset ? void 0 : page || 1,
1433
+ after: useKeyset ? after : void 0,
1425
1434
  limit,
1426
1435
  select: paginationOptions.select,
1427
1436
  session: options.session,
1428
- readPreference: paginationOptions.readPreference
1437
+ readPreference: paginationOptions.readPreference,
1438
+ collation: paginationOptions.collation,
1439
+ countStrategy: context.countStrategy ?? params.countStrategy
1429
1440
  });
1430
- const totalPages = Math.ceil((lookupResult.total ?? 0) / (lookupResult.limit ?? limit));
1431
- const currentPage = lookupResult.page ?? 1;
1432
- const result = {
1433
- method: "offset",
1441
+ let result;
1442
+ if (lookupResult.next !== void 0) result = {
1443
+ method: "keyset",
1434
1444
  docs: lookupResult.data,
1435
- page: currentPage,
1436
1445
  limit: lookupResult.limit ?? limit,
1437
- total: lookupResult.total ?? 0,
1438
- pages: totalPages,
1439
- hasNext: currentPage < totalPages,
1440
- hasPrev: currentPage > 1
1446
+ hasMore: lookupResult.hasMore ?? false,
1447
+ next: lookupResult.next ?? null
1441
1448
  };
1449
+ else {
1450
+ const total = lookupResult.total ?? 0;
1451
+ const resultLimit = lookupResult.limit ?? limit;
1452
+ const totalPages = Math.ceil(total / resultLimit);
1453
+ const currentPage = lookupResult.page ?? 1;
1454
+ const hasNext = lookupResult.hasMore !== void 0 ? lookupResult.hasMore : currentPage < totalPages;
1455
+ result = {
1456
+ method: "offset",
1457
+ docs: lookupResult.data,
1458
+ page: currentPage,
1459
+ limit: resultLimit,
1460
+ total,
1461
+ pages: totalPages,
1462
+ hasNext,
1463
+ hasPrev: currentPage > 1
1464
+ };
1465
+ }
1442
1466
  await this._emitHook("after:getAll", {
1443
1467
  context,
1444
1468
  result
@@ -1741,42 +1765,121 @@ var Repository = class {
1741
1765
  async lookupPopulate(options) {
1742
1766
  const context = await this._buildContext("lookupPopulate", options);
1743
1767
  try {
1744
- const builder = new AggregationBuilder();
1768
+ const MAX_LOOKUPS = 10;
1769
+ const lookups = context.lookups ?? options.lookups;
1770
+ if (lookups.length > MAX_LOOKUPS) throw createError(400, `Too many lookups (${lookups.length}). Maximum is ${MAX_LOOKUPS}.`);
1745
1771
  const filters = context.filters ?? options.filters;
1746
- if (filters && Object.keys(filters).length > 0) builder.match(filters);
1747
- builder.multiLookup(options.lookups);
1748
1772
  const sort = context.sort ?? options.sort;
1749
- if (sort) builder.sort(this._parseSort(sort));
1750
- const page = context.page ?? options.page ?? 1;
1751
1773
  const limit = context.limit ?? options.limit ?? this._pagination.config.defaultLimit ?? 20;
1752
- const skip = (page - 1) * limit;
1753
- const SAFE_LIMIT = 1e3;
1754
- const SAFE_MAX_OFFSET = 1e4;
1755
- if (limit > SAFE_LIMIT) warn(`[mongokit] Large limit (${limit}) in lookupPopulate. $facet results must be <16MB. Consider using smaller limits or stream-based pagination for large datasets.`);
1756
- if (skip > SAFE_MAX_OFFSET) warn(`[mongokit] Large offset (${skip}) in lookupPopulate. $facet with high offsets can exceed 16MB. For deep pagination, consider using keyset/cursor-based pagination instead.`);
1757
- const dataStages = [{ $skip: skip }, { $limit: limit }];
1774
+ const readPref = context.readPreference ?? options.readPreference;
1775
+ const session = context.session ?? options.session;
1776
+ const collation = context.collation ?? options.collation;
1777
+ const after = context.after ?? options.after;
1778
+ const pageFromContext = context.page ?? options.page;
1779
+ const isKeyset = !!after || !pageFromContext && !!sort;
1780
+ const countStrategy = context.countStrategy ?? options.countStrategy ?? "exact";
1758
1781
  const selectSpec = context.select ?? options.select;
1782
+ let projection;
1759
1783
  if (selectSpec) {
1760
- let projection;
1761
1784
  if (typeof selectSpec === "string") {
1762
1785
  projection = {};
1763
- const fields = selectSpec.split(",").map((f) => f.trim());
1764
- for (const field of fields) if (field.startsWith("-")) projection[field.substring(1)] = 0;
1786
+ for (const field of selectSpec.split(",").map((f) => f.trim())) if (field.startsWith("-")) projection[field.substring(1)] = 0;
1765
1787
  else projection[field] = 1;
1766
1788
  } else if (Array.isArray(selectSpec)) {
1767
1789
  projection = {};
1768
1790
  for (const field of selectSpec) if (field.startsWith("-")) projection[field.substring(1)] = 0;
1769
1791
  else projection[field] = 1;
1770
- } else projection = selectSpec;
1771
- dataStages.push({ $project: projection });
1792
+ } else projection = { ...selectSpec };
1793
+ if (Object.values(projection).some((v) => v === 1)) for (const lookup of lookups) {
1794
+ const asField = lookup.as || lookup.from;
1795
+ if (!(asField in projection)) projection[asField] = 1;
1796
+ }
1772
1797
  }
1773
- builder.facet({
1774
- metadata: [{ $count: "total" }],
1775
- data: dataStages
1776
- });
1777
- const pipeline = builder.build();
1778
- const aggregation = this.Model.aggregate(pipeline).session(options.session || null);
1779
- const readPref = context.readPreference ?? options.readPreference;
1798
+ const appendLookupStages = (pipeline) => {
1799
+ pipeline.push(...LookupBuilder.multiple(lookups));
1800
+ for (const lookup of lookups) if (lookup.single) {
1801
+ const asField = lookup.as || lookup.from;
1802
+ pipeline.push({ $addFields: { [asField]: { $ifNull: [`$${asField}`, null] } } });
1803
+ }
1804
+ const finalProjection = ensureLookupProjectionIncludesCursorFields(projection, isKeyset && sort ? this._parseSort(sort) : void 0);
1805
+ if (finalProjection) pipeline.push({ $project: finalProjection });
1806
+ };
1807
+ if (isKeyset && sort) {
1808
+ const parsedSort = this._parseSort(sort);
1809
+ const { validateKeysetSort } = await import("./sort-C-BJEWUZ.mjs").then((n) => n.n);
1810
+ const { encodeCursor, resolveCursorFilter } = await import("./cursor-CHToazHy.mjs").then((n) => n.t);
1811
+ const { getPrimaryField } = await import("./sort-C-BJEWUZ.mjs").then((n) => n.n);
1812
+ const normalizedSort = validateKeysetSort(parsedSort);
1813
+ const cursorVersion = this._pagination.config.cursorVersion ?? 1;
1814
+ const matchFilters = after ? resolveCursorFilter(after, normalizedSort, cursorVersion, { ...filters || {} }) : { ...filters || {} };
1815
+ if (projection) {
1816
+ if (Object.values(projection).some((v) => v === 1)) {
1817
+ for (const sortField of Object.keys(normalizedSort)) if (!(sortField in projection)) projection[sortField] = 1;
1818
+ }
1819
+ }
1820
+ const pipeline = [];
1821
+ if (Object.keys(matchFilters).length > 0) pipeline.push({ $match: matchFilters });
1822
+ pipeline.push({ $sort: normalizedSort });
1823
+ pipeline.push({ $limit: limit + 1 });
1824
+ appendLookupStages(pipeline);
1825
+ const aggregation = this.Model.aggregate(pipeline).session(session || null);
1826
+ if (collation) aggregation.collation(collation);
1827
+ if (readPref) aggregation.read(readPref);
1828
+ const docs = await aggregation;
1829
+ const hasMore = docs.length > limit;
1830
+ if (hasMore) docs.pop();
1831
+ const primaryField = getPrimaryField(normalizedSort);
1832
+ const nextCursor = hasMore && docs.length > 0 ? encodeCursor(docs[docs.length - 1], primaryField, normalizedSort, cursorVersion) : null;
1833
+ await this._emitHook("after:lookupPopulate", {
1834
+ context,
1835
+ result: docs
1836
+ });
1837
+ return {
1838
+ data: docs,
1839
+ total: 0,
1840
+ limit,
1841
+ next: nextCursor,
1842
+ hasMore
1843
+ };
1844
+ }
1845
+ const page = pageFromContext ?? 1;
1846
+ const skip = (page - 1) * limit;
1847
+ if (skip > 1e4) warn(`[mongokit] Large offset (${skip}) in lookupPopulate. Consider using keyset pagination: getAll({ sort, after, limit, lookups })`);
1848
+ const dataPipeline = [];
1849
+ if (filters && Object.keys(filters).length > 0) dataPipeline.push({ $match: filters });
1850
+ if (sort) dataPipeline.push({ $sort: this._parseSort(sort) });
1851
+ if (countStrategy === "none") {
1852
+ dataPipeline.push({ $skip: skip }, { $limit: limit + 1 });
1853
+ appendLookupStages(dataPipeline);
1854
+ const aggregation = this.Model.aggregate(dataPipeline).session(session || null);
1855
+ if (collation) aggregation.collation(collation);
1856
+ if (readPref) aggregation.read(readPref);
1857
+ const docs = await aggregation;
1858
+ const hasNext = docs.length > limit;
1859
+ if (hasNext) docs.pop();
1860
+ await this._emitHook("after:lookupPopulate", {
1861
+ context,
1862
+ result: docs
1863
+ });
1864
+ return {
1865
+ data: docs,
1866
+ total: 0,
1867
+ page,
1868
+ limit,
1869
+ hasMore: hasNext
1870
+ };
1871
+ }
1872
+ dataPipeline.push({ $skip: skip }, { $limit: limit });
1873
+ appendLookupStages(dataPipeline);
1874
+ const countPipeline = [];
1875
+ if (filters && Object.keys(filters).length > 0) countPipeline.push({ $match: filters });
1876
+ countPipeline.push({ $count: "total" });
1877
+ const pipeline = [{ $facet: {
1878
+ metadata: countPipeline,
1879
+ data: dataPipeline
1880
+ } }];
1881
+ const aggregation = this.Model.aggregate(pipeline).session(session || null);
1882
+ if (collation) aggregation.collation(collation);
1780
1883
  if (readPref) aggregation.read(readPref);
1781
1884
  const result = (await aggregation)[0] || {
1782
1885
  metadata: [],
@@ -3569,7 +3672,7 @@ function softDeletePlugin(options = {}) {
3569
3672
  }, { priority: HOOK_PRIORITY.POLICY });
3570
3673
  if (addRestoreMethod) {
3571
3674
  const restoreMethod = async function(id, restoreOptions = {}) {
3572
- const context = await this._buildContext.call(this, "restore", {
3675
+ const context = await this._buildContext("restore", {
3573
3676
  id,
3574
3677
  ...restoreOptions
3575
3678
  });
@@ -3602,7 +3705,7 @@ function softDeletePlugin(options = {}) {
3602
3705
  }
3603
3706
  if (addGetDeletedMethod) {
3604
3707
  const getDeletedMethod = async function(params = {}, getDeletedOptions = {}) {
3605
- const context = await this._buildContext.call(this, "getDeleted", {
3708
+ const context = await this._buildContext("getDeleted", {
3606
3709
  ...params,
3607
3710
  ...getDeletedOptions
3608
3711
  });
@@ -3676,13 +3779,13 @@ function subdocumentPlugin() {
3676
3779
  * Add subdocument to array
3677
3780
  */
3678
3781
  repo.registerMethod("addSubdocument", async function(parentId, arrayPath, subData, options = {}) {
3679
- return this.update.call(this, parentId, { $push: { [arrayPath]: subData } }, options);
3782
+ return this.update(parentId, { $push: { [arrayPath]: subData } }, options);
3680
3783
  });
3681
3784
  /**
3682
3785
  * Get subdocument from array
3683
3786
  */
3684
3787
  repo.registerMethod("getSubdocument", async function(parentId, arrayPath, subId, options = {}) {
3685
- return this._executeQuery.call(this, async (Model) => {
3788
+ return this._executeQuery(async (Model) => {
3686
3789
  const parent = await Model.findById(parentId).session(options.session).exec();
3687
3790
  if (!parent) throw createError(404, "Parent not found");
3688
3791
  const arrayField = parent[arrayPath];
@@ -3696,7 +3799,7 @@ function subdocumentPlugin() {
3696
3799
  * Update subdocument in array
3697
3800
  */
3698
3801
  repo.registerMethod("updateSubdocument", async function(parentId, arrayPath, subId, updateData, options = {}) {
3699
- return this._executeQuery.call(this, async (Model) => {
3802
+ return this._executeQuery(async (Model) => {
3700
3803
  const query = {
3701
3804
  _id: parentId,
3702
3805
  [`${arrayPath}._id`]: subId
@@ -3718,7 +3821,7 @@ function subdocumentPlugin() {
3718
3821
  * Delete subdocument from array
3719
3822
  */
3720
3823
  repo.registerMethod("deleteSubdocument", async function(parentId, arrayPath, subId, options = {}) {
3721
- return this.update.call(this, parentId, { $pull: { [arrayPath]: { _id: subId } } }, options);
3824
+ return this.update(parentId, { $pull: { [arrayPath]: { _id: subId } } }, options);
3722
3825
  });
3723
3826
  }
3724
3827
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "description": "Production-grade MongoDB repositories with zero dependencies - smart pagination, events, and plugins",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -84,7 +84,7 @@
84
84
  "mongoose": "^9.0.0"
85
85
  },
86
86
  "engines": {
87
- "node": ">=18"
87
+ "node": ">=22"
88
88
  },
89
89
  "scripts": {
90
90
  "build": "tsdown",
@@ -96,7 +96,7 @@
96
96
  "lint": "biome check src/",
97
97
  "lint:fix": "biome check src/ --write",
98
98
  "format": "biome format src/ --write",
99
- "check": "biome ci src/",
99
+ "check": "biome ci src/ --diagnostic-level=error",
100
100
  "prepublishOnly": "npm run check && npm run build && npm run typecheck && npm test",
101
101
  "publish:dry": "npm publish --dry-run --access public",
102
102
  "publish:npm": "npm publish --access public",