@classytic/mongokit 3.0.6 → 3.1.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.
@@ -0,0 +1,470 @@
1
+ import { create_exports } from './chunk-CF6FLC2G.js';
2
+ import { __export, createError } from './chunk-VJXDGP3C.js';
3
+
4
+ // src/actions/index.ts
5
+ var actions_exports = {};
6
+ __export(actions_exports, {
7
+ aggregate: () => aggregate_exports,
8
+ create: () => create_exports,
9
+ deleteActions: () => delete_exports,
10
+ read: () => read_exports,
11
+ update: () => update_exports
12
+ });
13
+
14
+ // src/actions/read.ts
15
+ var read_exports = {};
16
+ __export(read_exports, {
17
+ count: () => count,
18
+ exists: () => exists,
19
+ getAll: () => getAll,
20
+ getById: () => getById,
21
+ getByQuery: () => getByQuery,
22
+ getOrCreate: () => getOrCreate,
23
+ tryGetByQuery: () => tryGetByQuery
24
+ });
25
+ function parsePopulate(populate) {
26
+ if (!populate) return [];
27
+ if (typeof populate === "string") {
28
+ return populate.split(",").map((p) => p.trim());
29
+ }
30
+ if (Array.isArray(populate)) {
31
+ return populate.map((p) => typeof p === "string" ? p.trim() : p);
32
+ }
33
+ return [populate];
34
+ }
35
+ async function getById(Model, id, options = {}) {
36
+ const query = options.query ? Model.findOne({ _id: id, ...options.query }) : Model.findById(id);
37
+ if (options.select) query.select(options.select);
38
+ if (options.populate) query.populate(parsePopulate(options.populate));
39
+ if (options.lean) query.lean();
40
+ if (options.session) query.session(options.session);
41
+ const document = await query.exec();
42
+ if (!document && options.throwOnNotFound !== false) {
43
+ throw createError(404, "Document not found");
44
+ }
45
+ return document;
46
+ }
47
+ async function getByQuery(Model, query, options = {}) {
48
+ const mongoQuery = Model.findOne(query);
49
+ if (options.select) mongoQuery.select(options.select);
50
+ if (options.populate) mongoQuery.populate(parsePopulate(options.populate));
51
+ if (options.lean) mongoQuery.lean();
52
+ if (options.session) mongoQuery.session(options.session);
53
+ const document = await mongoQuery.exec();
54
+ if (!document && options.throwOnNotFound !== false) {
55
+ throw createError(404, "Document not found");
56
+ }
57
+ return document;
58
+ }
59
+ async function tryGetByQuery(Model, query, options = {}) {
60
+ return getByQuery(Model, query, { ...options, throwOnNotFound: false });
61
+ }
62
+ async function getAll(Model, query = {}, options = {}) {
63
+ let mongoQuery = Model.find(query);
64
+ if (options.select) mongoQuery = mongoQuery.select(options.select);
65
+ if (options.populate) mongoQuery = mongoQuery.populate(parsePopulate(options.populate));
66
+ if (options.sort) mongoQuery = mongoQuery.sort(options.sort);
67
+ if (options.limit) mongoQuery = mongoQuery.limit(options.limit);
68
+ if (options.skip) mongoQuery = mongoQuery.skip(options.skip);
69
+ mongoQuery = mongoQuery.lean(options.lean !== false);
70
+ if (options.session) mongoQuery = mongoQuery.session(options.session);
71
+ return mongoQuery.exec();
72
+ }
73
+ async function getOrCreate(Model, query, createData, options = {}) {
74
+ return Model.findOneAndUpdate(
75
+ query,
76
+ { $setOnInsert: createData },
77
+ {
78
+ upsert: true,
79
+ new: true,
80
+ runValidators: true,
81
+ session: options.session,
82
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
83
+ }
84
+ );
85
+ }
86
+ async function count(Model, query = {}, options = {}) {
87
+ return Model.countDocuments(query).session(options.session ?? null);
88
+ }
89
+ async function exists(Model, query, options = {}) {
90
+ return Model.exists(query).session(options.session ?? null);
91
+ }
92
+
93
+ // src/actions/update.ts
94
+ var update_exports = {};
95
+ __export(update_exports, {
96
+ increment: () => increment,
97
+ pullFromArray: () => pullFromArray,
98
+ pushToArray: () => pushToArray,
99
+ update: () => update,
100
+ updateByQuery: () => updateByQuery,
101
+ updateMany: () => updateMany,
102
+ updateWithConstraints: () => updateWithConstraints,
103
+ updateWithValidation: () => updateWithValidation
104
+ });
105
+ function assertUpdatePipelineAllowed(update2, updatePipeline) {
106
+ if (Array.isArray(update2) && updatePipeline !== true) {
107
+ throw createError(
108
+ 400,
109
+ "Update pipelines (array updates) are disabled by default; pass `{ updatePipeline: true }` to explicitly allow pipeline-style updates."
110
+ );
111
+ }
112
+ }
113
+ function parsePopulate2(populate) {
114
+ if (!populate) return [];
115
+ if (typeof populate === "string") {
116
+ return populate.split(",").map((p) => p.trim());
117
+ }
118
+ if (Array.isArray(populate)) {
119
+ return populate.map((p) => typeof p === "string" ? p.trim() : p);
120
+ }
121
+ return [populate];
122
+ }
123
+ async function update(Model, id, data, options = {}) {
124
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
125
+ const document = await Model.findByIdAndUpdate(id, data, {
126
+ new: true,
127
+ runValidators: true,
128
+ session: options.session,
129
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
130
+ }).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
131
+ if (!document) {
132
+ throw createError(404, "Document not found");
133
+ }
134
+ return document;
135
+ }
136
+ async function updateWithConstraints(Model, id, data, constraints = {}, options = {}) {
137
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
138
+ const query = { _id: id, ...constraints };
139
+ const document = await Model.findOneAndUpdate(query, data, {
140
+ new: true,
141
+ runValidators: true,
142
+ session: options.session,
143
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
144
+ }).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
145
+ return document;
146
+ }
147
+ async function updateWithValidation(Model, id, data, validationOptions = {}, options = {}) {
148
+ const { buildConstraints, validateUpdate } = validationOptions;
149
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
150
+ if (buildConstraints) {
151
+ const constraints = buildConstraints(data);
152
+ const document = await updateWithConstraints(Model, id, data, constraints, options);
153
+ if (document) {
154
+ return { success: true, data: document };
155
+ }
156
+ }
157
+ const existing = await Model.findById(id).select(options.select || "").lean();
158
+ if (!existing) {
159
+ return {
160
+ success: false,
161
+ error: {
162
+ code: 404,
163
+ message: "Document not found"
164
+ }
165
+ };
166
+ }
167
+ if (validateUpdate) {
168
+ const validation = validateUpdate(existing, data);
169
+ if (!validation.valid) {
170
+ return {
171
+ success: false,
172
+ error: {
173
+ code: 403,
174
+ message: validation.message || "Update not allowed",
175
+ violations: validation.violations
176
+ }
177
+ };
178
+ }
179
+ }
180
+ const updated = await update(Model, id, data, options);
181
+ return { success: true, data: updated };
182
+ }
183
+ async function updateMany(Model, query, data, options = {}) {
184
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
185
+ const result = await Model.updateMany(query, data, {
186
+ runValidators: true,
187
+ session: options.session,
188
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
189
+ });
190
+ return {
191
+ matchedCount: result.matchedCount,
192
+ modifiedCount: result.modifiedCount
193
+ };
194
+ }
195
+ async function updateByQuery(Model, query, data, options = {}) {
196
+ assertUpdatePipelineAllowed(data, options.updatePipeline);
197
+ const document = await Model.findOneAndUpdate(query, data, {
198
+ new: true,
199
+ runValidators: true,
200
+ session: options.session,
201
+ ...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
202
+ }).select(options.select || "").populate(parsePopulate2(options.populate)).lean(options.lean ?? false);
203
+ if (!document && options.throwOnNotFound !== false) {
204
+ throw createError(404, "Document not found");
205
+ }
206
+ return document;
207
+ }
208
+ async function increment(Model, id, field, value = 1, options = {}) {
209
+ return update(Model, id, { $inc: { [field]: value } }, options);
210
+ }
211
+ async function pushToArray(Model, id, field, value, options = {}) {
212
+ return update(Model, id, { $push: { [field]: value } }, options);
213
+ }
214
+ async function pullFromArray(Model, id, field, value, options = {}) {
215
+ return update(Model, id, { $pull: { [field]: value } }, options);
216
+ }
217
+
218
+ // src/actions/delete.ts
219
+ var delete_exports = {};
220
+ __export(delete_exports, {
221
+ deleteById: () => deleteById,
222
+ deleteByQuery: () => deleteByQuery,
223
+ deleteMany: () => deleteMany,
224
+ restore: () => restore,
225
+ softDelete: () => softDelete
226
+ });
227
+ async function deleteById(Model, id, options = {}) {
228
+ const document = await Model.findByIdAndDelete(id).session(options.session ?? null);
229
+ if (!document) {
230
+ throw createError(404, "Document not found");
231
+ }
232
+ return { success: true, message: "Deleted successfully" };
233
+ }
234
+ async function deleteMany(Model, query, options = {}) {
235
+ const result = await Model.deleteMany(query).session(options.session ?? null);
236
+ return {
237
+ success: true,
238
+ count: result.deletedCount,
239
+ message: "Deleted successfully"
240
+ };
241
+ }
242
+ async function deleteByQuery(Model, query, options = {}) {
243
+ const document = await Model.findOneAndDelete(query).session(options.session ?? null);
244
+ if (!document && options.throwOnNotFound !== false) {
245
+ throw createError(404, "Document not found");
246
+ }
247
+ return { success: true, message: "Deleted successfully" };
248
+ }
249
+ async function softDelete(Model, id, options = {}) {
250
+ const document = await Model.findByIdAndUpdate(
251
+ id,
252
+ {
253
+ deleted: true,
254
+ deletedAt: /* @__PURE__ */ new Date(),
255
+ deletedBy: options.userId
256
+ },
257
+ { new: true, session: options.session }
258
+ );
259
+ if (!document) {
260
+ throw createError(404, "Document not found");
261
+ }
262
+ return { success: true, message: "Soft deleted successfully" };
263
+ }
264
+ async function restore(Model, id, options = {}) {
265
+ const document = await Model.findByIdAndUpdate(
266
+ id,
267
+ {
268
+ deleted: false,
269
+ deletedAt: null,
270
+ deletedBy: null
271
+ },
272
+ { new: true, session: options.session }
273
+ );
274
+ if (!document) {
275
+ throw createError(404, "Document not found");
276
+ }
277
+ return { success: true, message: "Restored successfully" };
278
+ }
279
+
280
+ // src/actions/aggregate.ts
281
+ var aggregate_exports = {};
282
+ __export(aggregate_exports, {
283
+ aggregate: () => aggregate,
284
+ aggregatePaginate: () => aggregatePaginate,
285
+ average: () => average,
286
+ countBy: () => countBy,
287
+ distinct: () => distinct,
288
+ facet: () => facet,
289
+ groupBy: () => groupBy,
290
+ lookup: () => lookup,
291
+ minMax: () => minMax,
292
+ sum: () => sum,
293
+ unwind: () => unwind
294
+ });
295
+ async function aggregate(Model, pipeline, options = {}) {
296
+ const aggregation = Model.aggregate(pipeline);
297
+ if (options.session) {
298
+ aggregation.session(options.session);
299
+ }
300
+ return aggregation.exec();
301
+ }
302
+ async function aggregatePaginate(Model, pipeline, options = {}) {
303
+ const page = parseInt(String(options.page || 1), 10);
304
+ const limit = parseInt(String(options.limit || 10), 10);
305
+ const skip = (page - 1) * limit;
306
+ const SAFE_LIMIT = 1e3;
307
+ if (limit > SAFE_LIMIT) {
308
+ console.warn(
309
+ `[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`
310
+ );
311
+ }
312
+ const facetPipeline = [
313
+ ...pipeline,
314
+ {
315
+ $facet: {
316
+ docs: [{ $skip: skip }, { $limit: limit }],
317
+ total: [{ $count: "count" }]
318
+ }
319
+ }
320
+ ];
321
+ const aggregation = Model.aggregate(facetPipeline);
322
+ if (options.session) {
323
+ aggregation.session(options.session);
324
+ }
325
+ const [result] = await aggregation.exec();
326
+ const docs = result.docs || [];
327
+ const total = result.total[0]?.count || 0;
328
+ const pages = Math.ceil(total / limit);
329
+ return {
330
+ docs,
331
+ total,
332
+ page,
333
+ limit,
334
+ pages,
335
+ hasNext: page < pages,
336
+ hasPrev: page > 1
337
+ };
338
+ }
339
+ async function groupBy(Model, field, options = {}) {
340
+ const pipeline = [
341
+ { $group: { _id: `$${field}`, count: { $sum: 1 } } },
342
+ { $sort: { count: -1 } }
343
+ ];
344
+ if (options.limit) {
345
+ pipeline.push({ $limit: options.limit });
346
+ }
347
+ return aggregate(Model, pipeline, options);
348
+ }
349
+ async function countBy(Model, field, query = {}, options = {}) {
350
+ const pipeline = [];
351
+ if (Object.keys(query).length > 0) {
352
+ pipeline.push({ $match: query });
353
+ }
354
+ pipeline.push(
355
+ { $group: { _id: `$${field}`, count: { $sum: 1 } } },
356
+ { $sort: { count: -1 } }
357
+ );
358
+ return aggregate(Model, pipeline, options);
359
+ }
360
+ async function lookup(Model, lookupOptions) {
361
+ const { from, localField, foreignField, as, pipeline = [], let: letVars, query = {}, options = {} } = lookupOptions;
362
+ const aggPipeline = [];
363
+ if (Object.keys(query).length > 0) {
364
+ aggPipeline.push({ $match: query });
365
+ }
366
+ const usePipelineForm = pipeline.length > 0 || letVars;
367
+ if (usePipelineForm) {
368
+ if (pipeline.length === 0 && localField && foreignField) {
369
+ const autoPipeline = [
370
+ {
371
+ $match: {
372
+ $expr: {
373
+ $eq: [`$${foreignField}`, `$$${localField}`]
374
+ }
375
+ }
376
+ }
377
+ ];
378
+ aggPipeline.push({
379
+ $lookup: {
380
+ from,
381
+ let: { [localField]: `$${localField}`, ...letVars || {} },
382
+ pipeline: autoPipeline,
383
+ as
384
+ }
385
+ });
386
+ } else {
387
+ aggPipeline.push({
388
+ $lookup: {
389
+ from,
390
+ ...letVars && { let: letVars },
391
+ pipeline,
392
+ as
393
+ }
394
+ });
395
+ }
396
+ } else {
397
+ aggPipeline.push({
398
+ $lookup: {
399
+ from,
400
+ localField,
401
+ foreignField,
402
+ as
403
+ }
404
+ });
405
+ }
406
+ return aggregate(Model, aggPipeline, options);
407
+ }
408
+ async function unwind(Model, field, options = {}) {
409
+ const pipeline = [
410
+ {
411
+ $unwind: {
412
+ path: `$${field}`,
413
+ preserveNullAndEmptyArrays: options.preserveEmpty !== false
414
+ }
415
+ }
416
+ ];
417
+ return aggregate(Model, pipeline, { session: options.session });
418
+ }
419
+ async function facet(Model, facets, options = {}) {
420
+ const pipeline = [{ $facet: facets }];
421
+ return aggregate(Model, pipeline, options);
422
+ }
423
+ async function distinct(Model, field, query = {}, options = {}) {
424
+ return Model.distinct(field, query).session(options.session ?? null);
425
+ }
426
+ async function sum(Model, field, query = {}, options = {}) {
427
+ const pipeline = [];
428
+ if (Object.keys(query).length > 0) {
429
+ pipeline.push({ $match: query });
430
+ }
431
+ pipeline.push({
432
+ $group: {
433
+ _id: null,
434
+ total: { $sum: `$${field}` }
435
+ }
436
+ });
437
+ const result = await aggregate(Model, pipeline, options);
438
+ return result[0]?.total || 0;
439
+ }
440
+ async function average(Model, field, query = {}, options = {}) {
441
+ const pipeline = [];
442
+ if (Object.keys(query).length > 0) {
443
+ pipeline.push({ $match: query });
444
+ }
445
+ pipeline.push({
446
+ $group: {
447
+ _id: null,
448
+ average: { $avg: `$${field}` }
449
+ }
450
+ });
451
+ const result = await aggregate(Model, pipeline, options);
452
+ return result[0]?.average || 0;
453
+ }
454
+ async function minMax(Model, field, query = {}, options = {}) {
455
+ const pipeline = [];
456
+ if (Object.keys(query).length > 0) {
457
+ pipeline.push({ $match: query });
458
+ }
459
+ pipeline.push({
460
+ $group: {
461
+ _id: null,
462
+ min: { $min: `$${field}` },
463
+ max: { $max: `$${field}` }
464
+ }
465
+ });
466
+ const result = await aggregate(Model, pipeline, options);
467
+ return result[0] || { min: null, max: null };
468
+ }
469
+
470
+ export { actions_exports, aggregate, aggregate_exports, count, deleteById, delete_exports, distinct, exists, getById, getByQuery, getOrCreate, read_exports, update, update_exports };
@@ -0,0 +1,14 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/utils/error.ts
8
+ function createError(status, message) {
9
+ const error = new Error(message);
10
+ error.status = status;
11
+ return error;
12
+ }
13
+
14
+ export { __export, createError };
@@ -1,5 +1,166 @@
1
- import { Model, ClientSession, PipelineStage } from 'mongoose';
2
- import { A as AnyDocument, C as CreateOptions, h as ObjectId, n as OperationOptions, S as SelectSpec, e as PopulateSpec, f as SortSpec, U as UpdateOptions, p as UpdateWithValidationResult, o as UpdateManyResult, D as DeleteResult, X as GroupResult, T as LookupOptions, Y as MinMaxResult } from './types-DDDYo18H.js';
1
+ import { PipelineStage, ClientSession, Model } from 'mongoose';
2
+ import { A as AnyDocument, y as CreateOptions, k as ObjectId, x as OperationOptions, S as SelectSpec, e as PopulateSpec, f as SortSpec, l as UpdateOptions, E as UpdateWithValidationResult, B as UpdateManyResult, z as DeleteResult, a6 as GroupResult, a7 as MinMaxResult } from './types-DA0rs2Jh.js';
3
+
4
+ /**
5
+ * LookupBuilder - MongoDB $lookup Utility
6
+ *
7
+ * Standalone builder for efficient custom field joins using MongoDB $lookup aggregation.
8
+ * Optimized for millions of records with proper index usage.
9
+ *
10
+ * Features:
11
+ * - Join on custom fields (slugs, SKUs, codes, etc.)
12
+ * - Pipeline support for complex transformations
13
+ * - Index-aware query building
14
+ * - Single vs Array result handling
15
+ * - Nested lookups
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * // Simple lookup - join employees with departments by slug
20
+ * const lookup = new LookupBuilder('departments')
21
+ * .localField('departmentSlug')
22
+ * .foreignField('slug')
23
+ * .as('department')
24
+ * .single(); // Unwrap array to single object
25
+ *
26
+ * const pipeline = lookup.build();
27
+ * const results = await Employee.aggregate(pipeline);
28
+ *
29
+ * // Advanced lookup with pipeline
30
+ * const lookup = new LookupBuilder('products')
31
+ * .localField('productIds')
32
+ * .foreignField('sku')
33
+ * .pipeline([
34
+ * { $match: { status: 'active' } },
35
+ * { $project: { name: 1, price: 1 } }
36
+ * ])
37
+ * .as('products');
38
+ * ```
39
+ */
40
+
41
+ interface LookupOptions {
42
+ /** Collection to join with */
43
+ from: string;
44
+ /** Field from the input documents */
45
+ localField: string;
46
+ /** Field from the documents of the "from" collection */
47
+ foreignField: string;
48
+ /** Name of the new array field to add to the input documents */
49
+ as?: string;
50
+ /** Whether to unwrap array to single object */
51
+ single?: boolean;
52
+ /** Additional pipeline to run on the joined collection */
53
+ pipeline?: PipelineStage[];
54
+ /** Optional let variables for pipeline */
55
+ let?: Record<string, string>;
56
+ /** Query filter to apply before join (legacy, for aggregate.ts compatibility) */
57
+ query?: Record<string, unknown>;
58
+ /** Query options (legacy, for aggregate.ts compatibility) */
59
+ options?: {
60
+ session?: ClientSession;
61
+ };
62
+ }
63
+ /**
64
+ * Fluent builder for MongoDB $lookup aggregation stage
65
+ * Optimized for custom field joins at scale
66
+ */
67
+ declare class LookupBuilder {
68
+ private options;
69
+ constructor(from?: string);
70
+ /**
71
+ * Set the collection to join with
72
+ */
73
+ from(collection: string): this;
74
+ /**
75
+ * Set the local field (source collection)
76
+ * IMPORTANT: This field should be indexed for optimal performance
77
+ */
78
+ localField(field: string): this;
79
+ /**
80
+ * Set the foreign field (target collection)
81
+ * IMPORTANT: This field should be indexed (preferably unique) for optimal performance
82
+ */
83
+ foreignField(field: string): this;
84
+ /**
85
+ * Set the output field name
86
+ * Defaults to the collection name if not specified
87
+ */
88
+ as(fieldName: string): this;
89
+ /**
90
+ * Mark this lookup as returning a single document
91
+ * Automatically unwraps the array result to a single object or null
92
+ */
93
+ single(isSingle?: boolean): this;
94
+ /**
95
+ * Add a pipeline to filter/transform joined documents
96
+ * Useful for filtering, sorting, or limiting joined results
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * lookup.pipeline([
101
+ * { $match: { status: 'active' } },
102
+ * { $sort: { priority: -1 } },
103
+ * { $limit: 5 }
104
+ * ]);
105
+ * ```
106
+ */
107
+ pipeline(stages: PipelineStage[]): this;
108
+ /**
109
+ * Set let variables for use in pipeline
110
+ * Allows referencing local document fields in the pipeline
111
+ */
112
+ let(variables: Record<string, string>): this;
113
+ /**
114
+ * Build the $lookup aggregation stage(s)
115
+ * Returns an array of pipeline stages including $lookup and optional $unwind
116
+ *
117
+ * IMPORTANT: MongoDB $lookup has two mutually exclusive forms:
118
+ * 1. Simple form: { from, localField, foreignField, as }
119
+ * 2. Pipeline form: { from, let, pipeline, as }
120
+ *
121
+ * When pipeline or let is specified, we use the pipeline form.
122
+ * Otherwise, we use the simpler localField/foreignField form.
123
+ */
124
+ build(): PipelineStage[];
125
+ /**
126
+ * Build and return only the $lookup stage (without $unwind)
127
+ * Useful when you want to handle unwrapping yourself
128
+ */
129
+ buildLookupOnly(): PipelineStage.Lookup;
130
+ /**
131
+ * Static helper: Create a simple lookup in one line
132
+ */
133
+ static simple(from: string, localField: string, foreignField: string, options?: {
134
+ as?: string;
135
+ single?: boolean;
136
+ }): PipelineStage[];
137
+ /**
138
+ * Static helper: Create multiple lookups at once
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * const pipeline = LookupBuilder.multiple([
143
+ * { from: 'departments', localField: 'deptSlug', foreignField: 'slug', single: true },
144
+ * { from: 'managers', localField: 'managerId', foreignField: '_id', single: true }
145
+ * ]);
146
+ * ```
147
+ */
148
+ static multiple(lookups: LookupOptions[]): PipelineStage[];
149
+ /**
150
+ * Static helper: Create a nested lookup (lookup within lookup)
151
+ * Useful for multi-level joins like Order -> Product -> Category
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * // Join orders with products, then products with categories
156
+ * const pipeline = LookupBuilder.nested([
157
+ * { from: 'products', localField: 'productSku', foreignField: 'sku', as: 'product', single: true },
158
+ * { from: 'categories', localField: 'product.categorySlug', foreignField: 'slug', as: 'product.category', single: true }
159
+ * ]);
160
+ * ```
161
+ */
162
+ static nested(lookups: LookupOptions[]): PipelineStage[];
163
+ }
3
164
 
4
165
  /**
5
166
  * Create Actions
@@ -269,6 +430,12 @@ declare function countBy(Model: Model<any>, field: string, query?: Record<string
269
430
  }): Promise<GroupResult[]>;
270
431
  /**
271
432
  * Lookup (join) with another collection
433
+ *
434
+ * MongoDB $lookup has two mutually exclusive forms:
435
+ * 1. Simple form: { from, localField, foreignField, as }
436
+ * 2. Pipeline form: { from, let, pipeline, as }
437
+ *
438
+ * This function automatically selects the appropriate form based on parameters.
272
439
  */
273
440
  declare function lookup<TDoc = AnyDocument>(Model: Model<TDoc>, lookupOptions: LookupOptions): Promise<TDoc[]>;
274
441
  /**
@@ -334,4 +501,4 @@ declare namespace index {
334
501
  export { aggregate$1 as aggregate, create$1 as create, _delete as deleteActions, index_read as read, update$1 as update };
335
502
  }
336
503
 
337
- export { _delete as _, aggregate$1 as a, create$1 as c, index as i, read as r, update$1 as u };
504
+ export { type LookupOptions as L, _delete as _, LookupBuilder as a, aggregate$1 as b, create$1 as c, index as i, read as r, update$1 as u };