flongo 1.6.0 → 1.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
@@ -105,6 +105,17 @@ const results = await collection.getAll(
105
105
  query.orderBy('createdAt', SortDirection.Descending),
106
106
  { offset: 0, count: 20 }
107
107
  );
108
+
109
+ // Compound (multi-field) sort with a unique tiebreaker.
110
+ // Appending `_id` makes skip/limit pagination deterministic, so pages never
111
+ // overlap or skip documents when the primary sort key has ties.
112
+ const page = await collection.getAll(
113
+ new FlongoQuery()
114
+ .where('status').eq('accepted')
115
+ .orderBy('featured', SortDirection.Descending)
116
+ .thenBy('_id', SortDirection.Ascending), // => sort: { featured: -1, _id: 1 }
117
+ { offset: 0, count: 24 }
118
+ );
108
119
  ```
109
120
 
110
121
  ### Collection Operations
@@ -72,6 +72,16 @@ class CacheKeyGenerator {
72
72
  direction: query.orderDirection || types_1.SortDirection.Ascending
73
73
  };
74
74
  }
75
+ // Include the full ordered sort list only when tiebreakers are present, so
76
+ // queries differing only by a tiebreaker (e.g. `{ featured: -1 }` vs
77
+ // `{ featured: -1, _id: 1 }`) hash to distinct keys. Single-sort queries
78
+ // keep their previous key shape (just `order`) for cache continuity.
79
+ if (query.sorts && query.sorts.length > 1) {
80
+ normalized.sorts = query.sorts.map(sort => ({
81
+ field: sort.field,
82
+ direction: sort.direction || types_1.SortDirection.Ascending
83
+ }));
84
+ }
75
85
  if (query.orQueries && query.orQueries.length > 0) {
76
86
  normalized.or = query.orQueries
77
87
  .map(q => this.normalizeQuery(q))
package/dist/flongo.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { Db, MongoClient } from "mongodb";
1
+ import { Db, MongoClient, MongoClientOptions } from "mongodb";
2
2
  export declare let flongoClient: MongoClient;
3
3
  export declare let flongoDb: Db;
4
4
  export interface FlongoConfig {
5
5
  connectionString: string;
6
6
  dbName: string;
7
+ clientOptions?: MongoClientOptions;
7
8
  }
8
9
  export declare function initializeFlongo(config: FlongoConfig): void;
10
+ export declare function connectFlongo(config: FlongoConfig): Promise<void>;
9
11
  //# sourceMappingURL=flongo.d.ts.map
package/dist/flongo.js CHANGED
@@ -2,9 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.flongoDb = exports.flongoClient = void 0;
4
4
  exports.initializeFlongo = initializeFlongo;
5
+ exports.connectFlongo = connectFlongo;
5
6
  const mongodb_1 = require("mongodb");
7
+ const defaultClientOptions = {
8
+ serverSelectionTimeoutMS: 10000,
9
+ heartbeatFrequencyMS: 15000,
10
+ socketTimeoutMS: 30000,
11
+ maxIdleTimeMS: 60000,
12
+ };
6
13
  function initializeFlongo(config) {
7
- exports.flongoClient = new mongodb_1.MongoClient(config.connectionString);
14
+ const options = { ...defaultClientOptions, ...config.clientOptions };
15
+ exports.flongoClient = new mongodb_1.MongoClient(config.connectionString, options);
8
16
  exports.flongoDb = exports.flongoClient.db(config.dbName);
9
17
  }
18
+ async function connectFlongo(config) {
19
+ initializeFlongo(config);
20
+ await exports.flongoClient.connect();
21
+ }
10
22
  //# sourceMappingURL=flongo.js.map
@@ -1,4 +1,4 @@
1
- import { Bounds, Coordinates, Pagination, ColExpression, ColRange, ICollectionQuery, Logic, SortDirection } from "./types";
1
+ import { Bounds, Coordinates, Pagination, ColExpression, ColRange, ICollectionQuery, Logic, Sort, SortDirection } from "./types";
2
2
  import { Document, Filter, FindOptions } from "mongodb";
3
3
  /**
4
4
  * FlongoQuery provides a fluent, chainable interface for building MongoDB queries
@@ -19,10 +19,21 @@ export declare class FlongoQuery implements ICollectionQuery {
19
19
  expressions: ColExpression[];
20
20
  /** Array of range queries (currently unused but reserved for future features) */
21
21
  ranges: ColRange[];
22
- /** Field to sort results by */
23
- orderField?: string;
24
- /** Direction for sorting (ascending or descending) */
25
- orderDirection?: SortDirection;
22
+ /**
23
+ * Ordered list of sort keys. The first entry is the primary sort, subsequent
24
+ * entries act as tiebreakers (in order). Populated by `orderBy()`/`thenBy()`.
25
+ */
26
+ sorts: Sort[];
27
+ /**
28
+ * Primary sort field, derived from `sorts[0]`.
29
+ * @deprecated Retained for backward compatibility; use `sorts`.
30
+ */
31
+ get orderField(): string | undefined;
32
+ /**
33
+ * Primary sort direction, derived from `sorts[0]`.
34
+ * @deprecated Retained for backward compatibility; use `sorts`.
35
+ */
36
+ get orderDirection(): SortDirection | undefined;
26
37
  /** Array of queries to be combined with OR logic */
27
38
  orQueries: FlongoQuery[];
28
39
  /** Array of queries to be combined with AND logic */
@@ -224,12 +235,34 @@ export declare class FlongoQuery implements ICollectionQuery {
224
235
  */
225
236
  near(center: Coordinates, maxDistanceMeters: number): FlongoQuery;
226
237
  /**
227
- * Sets the field and direction for sorting results
238
+ * Sets the primary sort field and direction, resetting any previously
239
+ * configured sort keys (including tiebreakers added via `thenBy`).
228
240
  * @param field - Field name to sort by
229
241
  * @param direction - Sort direction (ascending or descending)
230
242
  * @returns This query instance for chaining
231
243
  */
232
244
  orderBy(field?: string, direction?: SortDirection): FlongoQuery;
245
+ /**
246
+ * Appends a secondary (tertiary, …) sort key used as a tiebreaker after the
247
+ * primary `orderBy` field. Sort keys are applied in the order they are added,
248
+ * which is what makes deterministic pagination possible (e.g. appending a
249
+ * unique `_id` tiebreaker so `skip`/`limit` pages don't overlap).
250
+ *
251
+ * If called before any `orderBy`, the field becomes the primary sort.
252
+ * If the same field is added more than once, the latest direction wins
253
+ * (the field keeps its original position in the sort order).
254
+ *
255
+ * @example
256
+ * new FlongoQuery()
257
+ * .orderBy('featured', SortDirection.Descending)
258
+ * .thenBy('_id', SortDirection.Ascending);
259
+ * // => sort: { featured: -1, _id: 1 }
260
+ *
261
+ * @param field - Field name to sort by
262
+ * @param direction - Sort direction (ascending or descending)
263
+ * @returns This query instance for chaining
264
+ */
265
+ thenBy(field?: string, direction?: SortDirection): FlongoQuery;
233
266
  /**
234
267
  * Adds a sub-query with AND logic
235
268
  * @param query - Sub-query to combine with AND logic
@@ -26,11 +26,30 @@ class FlongoQuery {
26
26
  this.expressions = [];
27
27
  /** Array of range queries (currently unused but reserved for future features) */
28
28
  this.ranges = [];
29
+ /**
30
+ * Ordered list of sort keys. The first entry is the primary sort, subsequent
31
+ * entries act as tiebreakers (in order). Populated by `orderBy()`/`thenBy()`.
32
+ */
33
+ this.sorts = [];
29
34
  /** Array of queries to be combined with OR logic */
30
35
  this.orQueries = [];
31
36
  /** Array of queries to be combined with AND logic */
32
37
  this.andQueries = [];
33
38
  }
39
+ /**
40
+ * Primary sort field, derived from `sorts[0]`.
41
+ * @deprecated Retained for backward compatibility; use `sorts`.
42
+ */
43
+ get orderField() {
44
+ return this.sorts[0]?.field;
45
+ }
46
+ /**
47
+ * Primary sort direction, derived from `sorts[0]`.
48
+ * @deprecated Retained for backward compatibility; use `sorts`.
49
+ */
50
+ get orderDirection() {
51
+ return this.sorts[0]?.direction;
52
+ }
34
53
  /**
35
54
  * Gets the most recently added expression for method chaining
36
55
  * @private
@@ -352,14 +371,47 @@ class FlongoQuery {
352
371
  // SORTING
353
372
  // ===========================================
354
373
  /**
355
- * Sets the field and direction for sorting results
374
+ * Sets the primary sort field and direction, resetting any previously
375
+ * configured sort keys (including tiebreakers added via `thenBy`).
356
376
  * @param field - Field name to sort by
357
377
  * @param direction - Sort direction (ascending or descending)
358
378
  * @returns This query instance for chaining
359
379
  */
360
380
  orderBy(field, direction) {
361
- this.orderField = field;
362
- this.orderDirection = direction;
381
+ this.sorts = field ? [{ field, direction }] : [];
382
+ return this;
383
+ }
384
+ /**
385
+ * Appends a secondary (tertiary, …) sort key used as a tiebreaker after the
386
+ * primary `orderBy` field. Sort keys are applied in the order they are added,
387
+ * which is what makes deterministic pagination possible (e.g. appending a
388
+ * unique `_id` tiebreaker so `skip`/`limit` pages don't overlap).
389
+ *
390
+ * If called before any `orderBy`, the field becomes the primary sort.
391
+ * If the same field is added more than once, the latest direction wins
392
+ * (the field keeps its original position in the sort order).
393
+ *
394
+ * @example
395
+ * new FlongoQuery()
396
+ * .orderBy('featured', SortDirection.Descending)
397
+ * .thenBy('_id', SortDirection.Ascending);
398
+ * // => sort: { featured: -1, _id: 1 }
399
+ *
400
+ * @param field - Field name to sort by
401
+ * @param direction - Sort direction (ascending or descending)
402
+ * @returns This query instance for chaining
403
+ */
404
+ thenBy(field, direction) {
405
+ if (!field) {
406
+ return this;
407
+ }
408
+ const existing = this.sorts.find((s) => s.field === field);
409
+ if (existing) {
410
+ existing.direction = direction;
411
+ }
412
+ else {
413
+ this.sorts.push({ field, direction });
414
+ }
363
415
  return this;
364
416
  }
365
417
  // ===========================================
@@ -486,11 +538,17 @@ class FlongoQuery {
486
538
  mongodbOptions.skip = pagination.offset;
487
539
  mongodbOptions.limit = pagination.count;
488
540
  }
489
- // Add sorting if specified
490
- if (this.orderField && this.orderDirection) {
491
- mongodbOptions.sort = {
492
- [this.orderField]: this.orderDirection === types_1.SortDirection.Ascending ? 1 : -1
493
- };
541
+ // Add sorting if specified. Iterate sort keys in order so MongoDB honors the
542
+ // insertion order of the resulting `sort` object's keys (primary first, then
543
+ // tiebreakers). Entries without a direction are skipped, preserving the prior
544
+ // behavior where a field set with no direction produced no sort.
545
+ const sortKeys = this.sorts.filter((s) => s.field && s.direction);
546
+ if (sortKeys.length) {
547
+ const sort = {};
548
+ for (const s of sortKeys) {
549
+ sort[s.field] = s.direction === types_1.SortDirection.Ascending ? 1 : -1;
550
+ }
551
+ mongodbOptions.sort = sort;
494
552
  }
495
553
  return mongodbOptions;
496
554
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { FlongoCollection, FlongoCollectionOptions } from "./flongoCollection";
2
2
  export { FlongoQuery, FlongoQueryBuilder } from "./flongoQuery";
3
- export { initializeFlongo, FlongoConfig, flongoClient, flongoDb } from "./flongo";
3
+ export { initializeFlongo, connectFlongo, FlongoConfig, flongoClient, flongoDb } from "./flongo";
4
4
  export { Error404, Error400 } from "./errors";
5
- export { Entity, DbRecord, Pagination, Coordinates, Bounds, Event, EventName, EventRecord, Logic, SortDirection, ColRange, ColExpression, ICollectionQuery, ICollection, Repository, CacheOptions, CachedCollectionOptions } from "./types";
5
+ export { Entity, DbRecord, Pagination, Coordinates, Bounds, Event, EventName, EventRecord, Logic, SortDirection, Sort, ColRange, ColExpression, ICollectionQuery, ICollection, Repository, CacheOptions, CachedCollectionOptions } from "./types";
6
6
  export { CacheStore, CacheEntry, CacheStats, CacheStoreOptions, BaseCacheStore, MemoryCache, MemoryCacheOptions, CacheKeyGenerator, CacheKeyOptions, InvalidationStrategy, InvalidationRule, InvalidationOptions, CacheInvalidator, TTLStrategy, LRUStrategy, CacheConfig, CacheProviderConfig, CacheConfiguration, createDefaultConfig, createProductionConfig, createDevelopmentConfig, DetailedCacheStats, CacheMetrics, CacheStatsCollector, CacheMonitor, getGlobalCacheMonitor, resetGlobalCacheMonitor } from "./cache";
7
7
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // Main exports for flongo package
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.resetGlobalCacheMonitor = exports.getGlobalCacheMonitor = exports.CacheMonitor = exports.CacheStatsCollector = exports.createDevelopmentConfig = exports.createProductionConfig = exports.createDefaultConfig = exports.CacheConfiguration = exports.LRUStrategy = exports.TTLStrategy = exports.CacheInvalidator = exports.InvalidationStrategy = exports.CacheKeyGenerator = exports.MemoryCache = exports.BaseCacheStore = exports.ColExpression = exports.ColRange = exports.SortDirection = exports.Logic = exports.EventName = exports.Error400 = exports.Error404 = exports.flongoDb = exports.flongoClient = exports.initializeFlongo = exports.FlongoQueryBuilder = exports.FlongoQuery = exports.FlongoCollection = void 0;
4
+ exports.resetGlobalCacheMonitor = exports.getGlobalCacheMonitor = exports.CacheMonitor = exports.CacheStatsCollector = exports.createDevelopmentConfig = exports.createProductionConfig = exports.createDefaultConfig = exports.CacheConfiguration = exports.LRUStrategy = exports.TTLStrategy = exports.CacheInvalidator = exports.InvalidationStrategy = exports.CacheKeyGenerator = exports.MemoryCache = exports.BaseCacheStore = exports.ColExpression = exports.ColRange = exports.SortDirection = exports.Logic = exports.EventName = exports.Error400 = exports.Error404 = exports.flongoDb = exports.flongoClient = exports.connectFlongo = exports.initializeFlongo = exports.FlongoQueryBuilder = exports.FlongoQuery = exports.FlongoCollection = void 0;
5
5
  var flongoCollection_1 = require("./flongoCollection");
6
6
  Object.defineProperty(exports, "FlongoCollection", { enumerable: true, get: function () { return flongoCollection_1.FlongoCollection; } });
7
7
  var flongoQuery_1 = require("./flongoQuery");
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "FlongoQuery", { enumerable: true, get: function
9
9
  Object.defineProperty(exports, "FlongoQueryBuilder", { enumerable: true, get: function () { return flongoQuery_1.FlongoQueryBuilder; } });
10
10
  var flongo_1 = require("./flongo");
11
11
  Object.defineProperty(exports, "initializeFlongo", { enumerable: true, get: function () { return flongo_1.initializeFlongo; } });
12
+ Object.defineProperty(exports, "connectFlongo", { enumerable: true, get: function () { return flongo_1.connectFlongo; } });
12
13
  Object.defineProperty(exports, "flongoClient", { enumerable: true, get: function () { return flongo_1.flongoClient; } });
13
14
  Object.defineProperty(exports, "flongoDb", { enumerable: true, get: function () { return flongo_1.flongoDb; } });
14
15
  var errors_1 = require("./errors");
package/dist/types.d.ts CHANGED
@@ -58,10 +58,19 @@ export declare class ColExpression {
58
58
  val: any;
59
59
  constructor(key: string);
60
60
  }
61
+ /** A single sort key: a field paired with a direction. */
62
+ export interface Sort {
63
+ field: string;
64
+ direction?: SortDirection;
65
+ }
61
66
  export type ICollectionQuery = {
62
67
  expressions: ColExpression[];
63
68
  ranges: ColRange[];
69
+ /** Ordered list of sort keys. The first entry is the primary sort. */
70
+ sorts?: Sort[];
71
+ /** @deprecated Primary sort field; derived from `sorts[0]`. Use `sorts`. */
64
72
  orderField?: string;
73
+ /** @deprecated Primary sort direction; derived from `sorts[0]`. Use `sorts`. */
65
74
  orderDirection?: SortDirection;
66
75
  orQueries: ICollectionQuery[];
67
76
  andQueries: ICollectionQuery[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flongo",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Firestore-like fluent query API for MongoDB",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",