flongo 1.9.0 โ†’ 2.0.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
@@ -47,6 +47,7 @@ const adults = await users.getAll(
47
47
  - ๐Ÿš€ **TypeScript support** - Full type safety and IntelliSense
48
48
  - ๐Ÿ“Š **Rich query operations** - Comparisons, arrays, geospatial, text search
49
49
  - ๐ŸŽ›๏ธ **Configurable** - Optional event logging, custom error handling
50
+ - ๐Ÿ—‚๏ธ **Declarative indexes** - Declare once, ensure idempotently on every boot
50
51
  - ๐Ÿงช **Battle-tested** - Extracted from production codebase
51
52
  - ๐Ÿ“ฆ **Zero config** - Works with existing MongoDB setup
52
53
 
@@ -204,18 +205,172 @@ await collection.batchCreate([user1, user2, user3]);
204
205
  ### Configuration
205
206
 
206
207
  ```typescript
207
- // Disable event logging
208
+ // Audit logging is OFF by default โ€” opt in per collection
208
209
  const collection = new FlongoCollection<User>('users', {
209
- enableEventLogging: false
210
+ enableEventLogging: true
210
211
  });
211
212
 
212
- // Custom events collection
213
+ // Opt in with a custom events collection
213
214
  const collection = new FlongoCollection<User>('users', {
214
215
  enableEventLogging: true,
215
216
  eventsCollectionName: 'audit_logs'
216
217
  });
217
218
  ```
218
219
 
220
+ #### Audit logging vs. your own `events` collection
221
+
222
+ Flongo's built-in audit trail is **opt-in** (`enableEventLogging` defaults to
223
+ `false`). When enabled, it writes to a collection named `events` by default. If
224
+ your application has its own `events` (analytics) collection, redirect Flongo's
225
+ audit trail to a dedicated collection so the two can be indexed and retained
226
+ independently:
227
+
228
+ ```typescript
229
+ const users = new FlongoCollection<User>('users', {
230
+ enableEventLogging: true,
231
+ eventsCollectionName: 'audit_events' // keep audit separate from app analytics
232
+ });
233
+ ```
234
+
235
+ Both `eventsCollectionName` and `enableEventLogging` are per-collection, so you
236
+ control audit behavior for each collection independently.
237
+
238
+ ## Index Management
239
+
240
+ Declare your indexes once in a central registry and Flongo will ensure them
241
+ **idempotently on every boot**. This keeps indexes colocated, version-controlled,
242
+ and out of ad-hoc scripts against the raw driver. Index management is **purely
243
+ additive** โ€” with no `indexes` registry declared, boot behavior is identical to
244
+ previous versions.
245
+
246
+ ### Declaring indexes
247
+
248
+ Register indexes at initialization, keyed by collection name. A single central
249
+ registry is the source of truth (rather than per-`FlongoCollection`-construction
250
+ specs), since one physical collection is often constructed in many places.
251
+
252
+ ```typescript
253
+ import { connectFlongo, syncFlongoIndexes } from 'flongo';
254
+
255
+ await connectFlongo({
256
+ connectionString: process.env.MONGO_URL!,
257
+ dbName: 'stays',
258
+ indexes: {
259
+ events: [
260
+ // Compound analytics indexes (order matters โ€” ESR: equality, sort, range)
261
+ { keys: { name: 1, 'value.stayId': 1, createdAt: 1 } },
262
+ { keys: { name: 1, identity: 1, createdAt: 1 } }
263
+ ],
264
+ users: [
265
+ { keys: { email: 1 }, options: { unique: true } }
266
+ ],
267
+ stays: [
268
+ { keys: { shortlink: 1 }, options: { unique: true, sparse: true } },
269
+ { keys: { 'location.coordinates.geoJSON': '2dsphere' } }
270
+ ]
271
+ },
272
+ indexSync: { mode: 'ensure', onError: 'warn', background: false }
273
+ });
274
+ ```
275
+
276
+ `connectFlongo` ensures the declared indexes after connecting. You can also run
277
+ the sync explicitly at any time (e.g. to gate boot on it, or from a migration /
278
+ CI job):
279
+
280
+ ```typescript
281
+ const report = await syncFlongoIndexes(); // idempotent; returns FlongoIndexReport[]
282
+ ```
283
+
284
+ ### Index spec
285
+
286
+ ```typescript
287
+ interface FlongoIndexSpec {
288
+ keys: Record<string, 1 | -1 | '2dsphere' | 'text' | 'hashed'>;
289
+ options?: {
290
+ name?: string; // custom index name
291
+ unique?: boolean;
292
+ sparse?: boolean;
293
+ partialFilterExpression?: Document; // partial index (e.g. unique-when-present)
294
+ expireAfterSeconds?: number; // TTL โ€” see caveat below
295
+ collation?: CollationOptions;
296
+ hidden?: boolean; // hide from the planner without dropping
297
+ };
298
+ }
299
+ ```
300
+
301
+ ### Sync semantics
302
+
303
+ `syncFlongoIndexes()` calls `createIndex(keys, options)` per spec and returns a
304
+ structured report:
305
+
306
+ ```typescript
307
+ interface FlongoIndexReport {
308
+ collection: string;
309
+ name: string; // resolved name
310
+ status: 'created' | 'exists' | 'conflict' | 'failed' | 'pruned';
311
+ error?: string;
312
+ }
313
+ ```
314
+
315
+ - **Identical** spec already present โ†’ `"exists"` (no-op).
316
+ - **Same keys, different options** โ†’ MongoDB throws `IndexOptionsConflict`;
317
+ Flongo records `"conflict"` and honors `onError` (it never silently
318
+ drops/recreates).
319
+ - **Creation error** (classic case: a `unique` index over a collection that
320
+ already contains duplicates) โ†’ `"failed"` with a message naming the likely
321
+ cause. By default this does **not** crash the process.
322
+
323
+ #### `indexSync` options
324
+
325
+ | Option | Values | Default | Behavior |
326
+ |---------------|---------------------------------|------------|----------|
327
+ | `mode` | `'ensure'` \| `'off'` \| `'strict'` | `'ensure'` | `ensure`: create missing, tolerate problems per `onError`. `off`: register specs but do nothing at boot (call `syncFlongoIndexes()` yourself). `strict`: throw on any conflict/failure (for CI / migration gates). |
328
+ | `onError` | `'warn'` \| `'throw'` | `'warn'` | How non-fatal problems are surfaced. `strict` mode always throws. |
329
+ | `background` | `boolean` | `false` | When `true`, boot does not block on index builds โ€” the sync runs asynchronously and logs its outcome. `await syncFlongoIndexes()` remains available when you want to await. |
330
+ | `prune` | `boolean` | `false` | Drop indexes present in Mongo but absent from the registry. See below. |
331
+ | `dryRun` | `boolean` | `false` | With `prune`, log what *would* be dropped without dropping. |
332
+
333
+ ### Non-destructive by default & pruning
334
+
335
+ Indexes present in Mongo but absent from the registry are **left untouched** by
336
+ default. Dropping is explicit opt-in and **never** touches the mandatory `_id_`
337
+ index:
338
+
339
+ ```typescript
340
+ // Dry run first โ€” logs out-of-registry indexes without dropping anything
341
+ await syncFlongoIndexes({ prune: true, dryRun: true });
342
+
343
+ // Then actually prune
344
+ const report = await syncFlongoIndexes({ prune: true });
345
+ ```
346
+
347
+ ### Verifying indexes
348
+
349
+ Two passthroughs help confirm indexes are applied and used:
350
+
351
+ ```typescript
352
+ const users = new FlongoCollection<User>('users');
353
+
354
+ await users.listIndexes(); // the collection's current index descriptions
355
+
356
+ // Confirm a query uses an index scan (IXSCAN) rather than a full scan (COLLSCAN)
357
+ const plan = await users.explain(new FlongoQuery().where('email').eq('a@b.com'));
358
+ ```
359
+
360
+ ### TTL caveat โš ๏ธ
361
+
362
+ `expireAfterSeconds` (TTL) requires the indexed field to be a **BSON `Date`**.
363
+ Flongo stamps `createdAt` / `updatedAt` as epoch **numbers** (`Date.now()`), so a
364
+ TTL index on `createdAt` **will not expire anything**. To use TTL, index a
365
+ dedicated `Date`-typed field that you write yourself:
366
+
367
+ ```typescript
368
+ await events.create({ ...data, expiresAt: new Date(Date.now() + 30 * 86400_000) });
369
+
370
+ // registry
371
+ events: [{ keys: { expiresAt: 1 }, options: { expireAfterSeconds: 0 } }]
372
+ ```
373
+
219
374
  ## Examples
220
375
 
221
376
  ### E-commerce Product Search
package/dist/flongo.d.ts CHANGED
@@ -1,10 +1,20 @@
1
1
  import { Db, MongoClient, MongoClientOptions } from "mongodb";
2
+ import { FlongoIndexRegistry, IndexSyncOptions } from "./indexes";
2
3
  export declare let flongoClient: MongoClient;
3
4
  export declare let flongoDb: Db;
4
5
  export interface FlongoConfig {
5
6
  connectionString: string;
6
7
  dbName: string;
7
8
  clientOptions?: MongoClientOptions;
9
+ /**
10
+ * Declarative index registry keyed by collection name. Indexes declared here
11
+ * are ensured at boot by `connectFlongo` (see `indexSync`) and can be
12
+ * re-applied any time via `syncFlongoIndexes()`. Omit for identical behavior
13
+ * to previous versions โ€” index management is purely additive.
14
+ */
15
+ indexes?: FlongoIndexRegistry;
16
+ /** Boot-time index sync behavior (mode, error handling, background, prune). */
17
+ indexSync?: IndexSyncOptions;
8
18
  }
9
19
  export declare function initializeFlongo(config: FlongoConfig): void;
10
20
  export declare function connectFlongo(config: FlongoConfig): Promise<void>;
package/dist/flongo.js CHANGED
@@ -4,6 +4,7 @@ exports.flongoDb = exports.flongoClient = void 0;
4
4
  exports.initializeFlongo = initializeFlongo;
5
5
  exports.connectFlongo = connectFlongo;
6
6
  const mongodb_1 = require("mongodb");
7
+ const indexes_1 = require("./indexes");
7
8
  const defaultClientOptions = {
8
9
  serverSelectionTimeoutMS: 10000,
9
10
  heartbeatFrequencyMS: 15000,
@@ -14,9 +15,16 @@ function initializeFlongo(config) {
14
15
  const options = { ...defaultClientOptions, ...config.clientOptions };
15
16
  exports.flongoClient = new mongodb_1.MongoClient(config.connectionString, options);
16
17
  exports.flongoDb = exports.flongoClient.db(config.dbName);
18
+ // Register declarative index specs so `syncFlongoIndexes()` / boot sync can
19
+ // find them. Registration is synchronous and connection-independent.
20
+ (0, indexes_1.registerFlongoIndexes)(config.indexes);
21
+ (0, indexes_1.setIndexSyncConfig)(config.indexSync);
17
22
  }
18
23
  async function connectFlongo(config) {
19
24
  initializeFlongo(config);
20
25
  await exports.flongoClient.connect();
26
+ // Ensure declared indexes once connected. Honors indexSync.mode/background;
27
+ // a no-op when no indexes are registered, so existing callers are unaffected.
28
+ await (0, indexes_1.runBootIndexSync)();
21
29
  }
22
30
  //# sourceMappingURL=flongo.js.map
@@ -1,6 +1,6 @@
1
1
  import { FlongoQuery } from "./flongoQuery";
2
2
  import { Entity, Event, Pagination, Repository } from "./types";
3
- import { Document } from "mongodb";
3
+ import { Document, IndexDescription } from "mongodb";
4
4
  /**
5
5
  * Clears the memoized per-connection server-version check. Exported for tests;
6
6
  * production code relies on the connection-keyed cache invalidating itself.
@@ -10,9 +10,18 @@ export declare function __resetServerVersionCache(): void;
10
10
  * Configuration options for FlongoCollection instances
11
11
  */
12
12
  export interface FlongoCollectionOptions {
13
- /** Whether to enable automatic event logging for CRUD operations */
13
+ /**
14
+ * Whether to enable automatic event logging (audit trail) for CRUD
15
+ * operations. Defaults to `false` โ€” audit logging is opt-in. Set to `true`
16
+ * per collection to record an audit trail (into `eventsCollectionName`).
17
+ */
14
18
  enableEventLogging?: boolean;
15
- /** Name of the collection to store events in (defaults to "events") */
19
+ /**
20
+ * Name of the collection audit events are written to. Defaults to `"events"`.
21
+ * If your app has its own `events`/analytics collection, redirect Flongo's
22
+ * audit trail to a dedicated collection (e.g. `"audit_events"`) so audit and
23
+ * analytics can be indexed and retained independently.
24
+ */
16
25
  eventsCollectionName?: string;
17
26
  }
18
27
  /**
@@ -234,6 +243,21 @@ export declare class FlongoCollection<T> {
234
243
  * @param clientId - Optional client ID for audit trail
235
244
  */
236
245
  arrRemove(id: string, key: string, items: any[], clientId?: string): Promise<void>;
246
+ /**
247
+ * Lists the indexes currently defined on this collection. A thin passthrough
248
+ * over the driver's `listIndexes()`, useful for verifying that declared
249
+ * indexes were applied (pairs with `syncFlongoIndexes()` and `explain()`).
250
+ * @returns Promise resolving to the collection's index descriptions
251
+ */
252
+ listIndexes(): Promise<IndexDescription[]>;
253
+ /**
254
+ * Returns the query planner's execution plan for a FlongoQuery. Pairs with
255
+ * index verification โ€” inspect the winning plan's stage to confirm an
256
+ * `IXSCAN` (index scan) rather than a `COLLSCAN` (full collection scan).
257
+ * @param query - The FlongoQuery to explain
258
+ * @returns Promise resolving to MongoDB's explain output
259
+ */
260
+ explain(query: FlongoQuery): Promise<Document>;
237
261
  /**
238
262
  * Logs an event to the events collection for audit trails
239
263
  * Events are only logged if event logging is enabled in options
@@ -84,7 +84,7 @@ class FlongoCollection {
84
84
  */
85
85
  constructor(collectionName, options = {}) {
86
86
  this.collection = flongo_1.flongoDb.collection(collectionName);
87
- this.options = { enableEventLogging: true, eventsCollectionName: "events", ...options };
87
+ this.options = { enableEventLogging: false, eventsCollectionName: "events", ...options };
88
88
  // Initialize events collection only if logging is enabled
89
89
  this.events = this.options.enableEventLogging
90
90
  ? flongo_1.flongoDb.collection(this.options.eventsCollectionName)
@@ -515,6 +515,30 @@ class FlongoCollection {
515
515
  });
516
516
  }
517
517
  // ===========================================
518
+ // INDEX INTROSPECTION
519
+ // ===========================================
520
+ /**
521
+ * Lists the indexes currently defined on this collection. A thin passthrough
522
+ * over the driver's `listIndexes()`, useful for verifying that declared
523
+ * indexes were applied (pairs with `syncFlongoIndexes()` and `explain()`).
524
+ * @returns Promise resolving to the collection's index descriptions
525
+ */
526
+ async listIndexes() {
527
+ return (await this.collection.listIndexes().toArray());
528
+ }
529
+ /**
530
+ * Returns the query planner's execution plan for a FlongoQuery. Pairs with
531
+ * index verification โ€” inspect the winning plan's stage to confirm an
532
+ * `IXSCAN` (index scan) rather than a `COLLSCAN` (full collection scan).
533
+ * @param query - The FlongoQuery to explain
534
+ * @returns Promise resolving to MongoDB's explain output
535
+ */
536
+ async explain(query) {
537
+ const mongodbQuery = query?.build() ?? {};
538
+ const mongodbOptions = query?.buildOptions() ?? {};
539
+ return this.collection.find(mongodbQuery, mongodbOptions).explain();
540
+ }
541
+ // ===========================================
518
542
  // EVENT LOGGING
519
543
  // ===========================================
520
544
  /**
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { FlongoCollection, FlongoCollectionOptions } from "./flongoCollection";
2
2
  export { FlongoQuery, FlongoQueryBuilder } from "./flongoQuery";
3
3
  export { initializeFlongo, connectFlongo, FlongoConfig, flongoClient, flongoDb } from "./flongo";
4
4
  export { Error404, Error400 } from "./errors";
5
+ export { syncFlongoIndexes, getFlongoIndexRegistry, defaultIndexName, FlongoIndexSpec, FlongoIndexOptions, FlongoIndexKeyType, FlongoIndexRegistry, FlongoIndexReport, FlongoIndexStatus, IndexSyncOptions, IndexSyncMode, IndexSyncOnError, SyncFlongoIndexesOptions } from "./indexes";
5
6
  export { Entity, DbRecord, Pagination, Coordinates, Bounds, Event, EventName, EventRecord, Logic, SortDirection, Sort, ColRange, ColExpression, ICollectionQuery, ICollection, Repository, CacheOptions, CachedCollectionOptions } from "./types";
6
7
  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
8
  //# 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.connectFlongo = 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.defaultIndexName = exports.getFlongoIndexRegistry = exports.syncFlongoIndexes = 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");
@@ -15,6 +15,11 @@ Object.defineProperty(exports, "flongoDb", { enumerable: true, get: function ()
15
15
  var errors_1 = require("./errors");
16
16
  Object.defineProperty(exports, "Error404", { enumerable: true, get: function () { return errors_1.Error404; } });
17
17
  Object.defineProperty(exports, "Error400", { enumerable: true, get: function () { return errors_1.Error400; } });
18
+ // Declarative index management
19
+ var indexes_1 = require("./indexes");
20
+ Object.defineProperty(exports, "syncFlongoIndexes", { enumerable: true, get: function () { return indexes_1.syncFlongoIndexes; } });
21
+ Object.defineProperty(exports, "getFlongoIndexRegistry", { enumerable: true, get: function () { return indexes_1.getFlongoIndexRegistry; } });
22
+ Object.defineProperty(exports, "defaultIndexName", { enumerable: true, get: function () { return indexes_1.defaultIndexName; } });
18
23
  var types_1 = require("./types");
19
24
  Object.defineProperty(exports, "EventName", { enumerable: true, get: function () { return types_1.EventName; } });
20
25
  Object.defineProperty(exports, "Logic", { enumerable: true, get: function () { return types_1.Logic; } });
@@ -0,0 +1,164 @@
1
+ import { CollationOptions, Document } from "mongodb";
2
+ /**
3
+ * The value of a single key in an index specification.
4
+ * - `1` / `-1` โ€” ascending / descending B-tree key.
5
+ * - `"2dsphere"` โ€” geospatial index (GeoJSON).
6
+ * - `"text"` โ€” full-text index.
7
+ * - `"hashed"` โ€” hashed index (e.g. for hashed sharding).
8
+ */
9
+ export type FlongoIndexKeyType = 1 | -1 | "2dsphere" | "text" | "hashed";
10
+ /**
11
+ * Options for a declared index. Mirrors the subset of the MongoDB driver's
12
+ * `CreateIndexesOptions` that Flongo supports declaratively. Any option here is
13
+ * passed through to `createIndex` verbatim.
14
+ */
15
+ export interface FlongoIndexOptions {
16
+ /** Custom index name. Defaults to MongoDB's generated `field_direction` name. */
17
+ name?: string;
18
+ /** Enforce uniqueness across the indexed key(s). */
19
+ unique?: boolean;
20
+ /** Only index documents that contain the indexed field(s). */
21
+ sparse?: boolean;
22
+ /**
23
+ * Only index documents matching this filter (partial index). Useful for
24
+ * "unique when present" constraints and for indexing a subset of documents
25
+ * (e.g. only analytics events).
26
+ */
27
+ partialFilterExpression?: Document;
28
+ /**
29
+ * TTL: seconds after which a document expires. **Requires the indexed field
30
+ * to be a BSON `Date`.** Flongo stamps `createdAt`/`updatedAt` as epoch
31
+ * numbers, so a TTL index on those fields will not expire anything โ€” index a
32
+ * dedicated `Date`-typed field instead. See the README TTL caveat.
33
+ */
34
+ expireAfterSeconds?: number;
35
+ /** Collation (locale-aware comparison) for the index. */
36
+ collation?: CollationOptions;
37
+ /** Hide the index from the query planner without dropping it. */
38
+ hidden?: boolean;
39
+ }
40
+ /**
41
+ * A declarative, idempotent index specification. Declared once in the registry
42
+ * and ensured on every boot via {@link syncFlongoIndexes}.
43
+ */
44
+ export interface FlongoIndexSpec {
45
+ /** The index key map. Keys are (dot-path) field names; values are key types. */
46
+ keys: Record<string, FlongoIndexKeyType>;
47
+ /** Optional index options (unique, sparse, TTL, collation, etc.). */
48
+ options?: FlongoIndexOptions;
49
+ }
50
+ /**
51
+ * The index registry: a map of collection name โ†’ declared index specs. A single
52
+ * central registry (rather than per-`FlongoCollection`-construction specs) is
53
+ * the source of truth, since one physical collection is frequently constructed
54
+ * in many places and per-construction specs would duplicate and conflict.
55
+ */
56
+ export type FlongoIndexRegistry = Record<string, FlongoIndexSpec[]>;
57
+ /**
58
+ * Boot-time sync behavior.
59
+ * - `"ensure"` (default) โ€” create missing indexes; tolerate conflicts/failures
60
+ * per `onError`.
61
+ * - `"off"` โ€” register specs but do nothing at boot; the app calls
62
+ * {@link syncFlongoIndexes} itself.
63
+ * - `"strict"` โ€” throw on any conflict or failure (for CI / migration gates).
64
+ */
65
+ export type IndexSyncMode = "ensure" | "off" | "strict";
66
+ /** How a non-fatal sync problem is surfaced: log and continue, or throw. */
67
+ export type IndexSyncOnError = "warn" | "throw";
68
+ /**
69
+ * Configuration for boot-time index sync, supplied via `initializeFlongo`'s
70
+ * `indexSync` field.
71
+ */
72
+ export interface IndexSyncOptions {
73
+ /** Sync mode. Defaults to `"ensure"`. */
74
+ mode?: IndexSyncMode;
75
+ /** Error handling for non-strict modes. Defaults to `"warn"`. */
76
+ onError?: IndexSyncOnError;
77
+ /**
78
+ * When `true`, boot does not block on index builds โ€” the sync is kicked off
79
+ * asynchronously and its outcome is logged on completion. `await
80
+ * syncFlongoIndexes()` remains available when the app wants to await. Defaults
81
+ * to `false` (await index builds during `connectFlongo`).
82
+ */
83
+ background?: boolean;
84
+ /**
85
+ * When `true`, drop indexes that exist in Mongo but are absent from the
86
+ * registry (never `_id_`). Opt-in and off by default. See also `dryRun`.
87
+ */
88
+ prune?: boolean;
89
+ /**
90
+ * When pruning, log the indexes that *would* be dropped without dropping
91
+ * them. Off by default.
92
+ */
93
+ dryRun?: boolean;
94
+ }
95
+ /** The outcome of ensuring (or pruning) a single index. */
96
+ export type FlongoIndexStatus = "created" | "exists" | "conflict" | "failed" | "pruned";
97
+ /** A structured, per-index result from {@link syncFlongoIndexes}. */
98
+ export interface FlongoIndexReport {
99
+ /** Collection the index belongs to. */
100
+ collection: string;
101
+ /** Resolved index name. */
102
+ name: string;
103
+ /** What happened to this index during the sync. */
104
+ status: FlongoIndexStatus;
105
+ /** Error/diagnostic message when `status` is `"conflict"` or `"failed"`. */
106
+ error?: string;
107
+ }
108
+ /** Per-call overrides for {@link syncFlongoIndexes}. */
109
+ export interface SyncFlongoIndexesOptions {
110
+ /** Override the configured sync mode. */
111
+ mode?: IndexSyncMode;
112
+ /** Override the configured error handling. */
113
+ onError?: IndexSyncOnError;
114
+ /** Drop out-of-registry indexes (never `_id_`). */
115
+ prune?: boolean;
116
+ /** With `prune`, log candidates without dropping. */
117
+ dryRun?: boolean;
118
+ }
119
+ /**
120
+ * Registers the declarative index registry. Called by `initializeFlongo`; a
121
+ * missing registry clears any previously registered specs.
122
+ */
123
+ export declare function registerFlongoIndexes(registry?: FlongoIndexRegistry): void;
124
+ /** Registers boot-time index sync configuration. Called by `initializeFlongo`. */
125
+ export declare function setIndexSyncConfig(config?: IndexSyncOptions): void;
126
+ /** Returns the currently registered index registry. */
127
+ export declare function getFlongoIndexRegistry(): FlongoIndexRegistry;
128
+ /** Returns the currently registered index sync configuration. */
129
+ export declare function getIndexSyncConfig(): IndexSyncOptions;
130
+ /**
131
+ * Computes the index name MongoDB would generate for a key map, matching the
132
+ * driver's `field_direction` join (e.g. `{ a: 1, "b.c": -1 }` โ†’ `a_1_b.c_-1`,
133
+ * `{ loc: "2dsphere" }` โ†’ `loc_2dsphere`). Used to resolve report names and to
134
+ * match declared specs against existing indexes without a round-trip.
135
+ */
136
+ export declare function defaultIndexName(keys: Record<string, FlongoIndexKeyType>): string;
137
+ /**
138
+ * Idempotently ensures every index in the registry exists, returning a
139
+ * structured report. Safe to run on every boot.
140
+ *
141
+ * For each spec, calls `createIndex(keys, options)`:
142
+ * - identical spec already present โ†’ `"exists"` (no-op);
143
+ * - same keys with different options โ†’ Mongo throws a conflict, recorded as
144
+ * `"conflict"` (never silently dropped/recreated);
145
+ * - other creation errors (e.g. a unique index over existing duplicates) โ†’
146
+ * `"failed"` with a diagnostic message.
147
+ *
148
+ * `mode`/`onError` are read from the `indexSync` config unless overridden here.
149
+ * `strict` mode forces `onError: "throw"`. With `prune`, indexes present in
150
+ * Mongo but absent from the registry are dropped (never `_id_`); `dryRun` logs
151
+ * candidates without dropping.
152
+ *
153
+ * @param opts - Per-call overrides for mode, onError, prune, and dryRun.
154
+ * @returns One {@link FlongoIndexReport} per ensured (and pruned) index.
155
+ */
156
+ export declare function syncFlongoIndexes(opts?: SyncFlongoIndexesOptions): Promise<FlongoIndexReport[]>;
157
+ /**
158
+ * Boot-time entry point invoked by `connectFlongo` after the connection is
159
+ * established. Respects `indexSync.mode` (`"off"` does nothing) and
160
+ * `indexSync.background` (kick off async and log on completion rather than
161
+ * blocking boot). A no-op when no indexes are registered.
162
+ */
163
+ export declare function runBootIndexSync(): Promise<void>;
164
+ //# sourceMappingURL=indexes.d.ts.map
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerFlongoIndexes = registerFlongoIndexes;
4
+ exports.setIndexSyncConfig = setIndexSyncConfig;
5
+ exports.getFlongoIndexRegistry = getFlongoIndexRegistry;
6
+ exports.getIndexSyncConfig = getIndexSyncConfig;
7
+ exports.defaultIndexName = defaultIndexName;
8
+ exports.syncFlongoIndexes = syncFlongoIndexes;
9
+ exports.runBootIndexSync = runBootIndexSync;
10
+ const flongo_1 = require("./flongo");
11
+ // ===========================================
12
+ // REGISTRY STATE
13
+ // ===========================================
14
+ let indexRegistry = {};
15
+ let indexSyncConfig = {};
16
+ /**
17
+ * Registers the declarative index registry. Called by `initializeFlongo`; a
18
+ * missing registry clears any previously registered specs.
19
+ */
20
+ function registerFlongoIndexes(registry) {
21
+ indexRegistry = registry ?? {};
22
+ }
23
+ /** Registers boot-time index sync configuration. Called by `initializeFlongo`. */
24
+ function setIndexSyncConfig(config) {
25
+ indexSyncConfig = config ?? {};
26
+ }
27
+ /** Returns the currently registered index registry. */
28
+ function getFlongoIndexRegistry() {
29
+ return indexRegistry;
30
+ }
31
+ /** Returns the currently registered index sync configuration. */
32
+ function getIndexSyncConfig() {
33
+ return indexSyncConfig;
34
+ }
35
+ // ===========================================
36
+ // HELPERS
37
+ // ===========================================
38
+ /**
39
+ * Computes the index name MongoDB would generate for a key map, matching the
40
+ * driver's `field_direction` join (e.g. `{ a: 1, "b.c": -1 }` โ†’ `a_1_b.c_-1`,
41
+ * `{ loc: "2dsphere" }` โ†’ `loc_2dsphere`). Used to resolve report names and to
42
+ * match declared specs against existing indexes without a round-trip.
43
+ */
44
+ function defaultIndexName(keys) {
45
+ return Object.entries(keys)
46
+ .map(([field, type]) => `${field}_${type}`)
47
+ .join("_");
48
+ }
49
+ /** Resolves a spec's effective index name (explicit `name` or the default). */
50
+ function resolveIndexName(spec) {
51
+ return spec.options?.name ?? defaultIndexName(spec.keys);
52
+ }
53
+ /**
54
+ * Lists a collection's existing indexes, treating a missing collection (which
55
+ * has no indexes yet) as an empty list rather than an error.
56
+ */
57
+ async function fetchExistingIndexes(collection) {
58
+ try {
59
+ return await collection.listIndexes().toArray();
60
+ }
61
+ catch (err) {
62
+ // NamespaceNotFound (code 26): the collection doesn't exist yet, so it has
63
+ // no indexes. Any create below will lazily create the collection.
64
+ if (err?.code === 26 || /ns does not exist/i.test(err?.message ?? "")) {
65
+ return [];
66
+ }
67
+ throw err;
68
+ }
69
+ }
70
+ /**
71
+ * Whether an error from `createIndex` represents an index *conflict* โ€” the same
72
+ * name or key pattern already exists with different options โ€” as opposed to a
73
+ * genuine creation failure.
74
+ */
75
+ function isConflictError(err) {
76
+ const code = err?.code;
77
+ const codeName = err?.codeName ?? "";
78
+ return (code === 85 || // IndexOptionsConflict
79
+ code === 86 || // IndexKeySpecsConflict
80
+ codeName === "IndexOptionsConflict" ||
81
+ codeName === "IndexKeySpecsConflict" ||
82
+ /already exists with different|different options/i.test(err?.message ?? ""));
83
+ }
84
+ /**
85
+ * Produces a diagnostic message for a failed index build, naming the likely
86
+ * cause for the classic case of a unique index over existing duplicates.
87
+ */
88
+ function describeFailure(err) {
89
+ const base = err?.message ?? String(err);
90
+ if (err?.code === 11000 || /duplicate key|E11000/i.test(base)) {
91
+ return `${base} โ€” likely cause: a unique index cannot be built because the collection already contains duplicate values for the indexed key(s). Resolve the duplicates or drop the \`unique\` option.`;
92
+ }
93
+ return base;
94
+ }
95
+ /** Logs a one-line summary of a sync run, grouped by status. */
96
+ function logSummary(reports) {
97
+ const counts = {};
98
+ for (const r of reports) {
99
+ counts[r.status] = (counts[r.status] ?? 0) + 1;
100
+ }
101
+ const summary = Object.entries(counts)
102
+ .map(([status, n]) => `${n} ${status}`)
103
+ .join(", ");
104
+ console.log(`[flongo] Index sync complete: ${summary || "no indexes declared"}`);
105
+ }
106
+ // ===========================================
107
+ // SYNC
108
+ // ===========================================
109
+ /**
110
+ * Idempotently ensures every index in the registry exists, returning a
111
+ * structured report. Safe to run on every boot.
112
+ *
113
+ * For each spec, calls `createIndex(keys, options)`:
114
+ * - identical spec already present โ†’ `"exists"` (no-op);
115
+ * - same keys with different options โ†’ Mongo throws a conflict, recorded as
116
+ * `"conflict"` (never silently dropped/recreated);
117
+ * - other creation errors (e.g. a unique index over existing duplicates) โ†’
118
+ * `"failed"` with a diagnostic message.
119
+ *
120
+ * `mode`/`onError` are read from the `indexSync` config unless overridden here.
121
+ * `strict` mode forces `onError: "throw"`. With `prune`, indexes present in
122
+ * Mongo but absent from the registry are dropped (never `_id_`); `dryRun` logs
123
+ * candidates without dropping.
124
+ *
125
+ * @param opts - Per-call overrides for mode, onError, prune, and dryRun.
126
+ * @returns One {@link FlongoIndexReport} per ensured (and pruned) index.
127
+ */
128
+ async function syncFlongoIndexes(opts = {}) {
129
+ const cfg = getIndexSyncConfig();
130
+ const mode = opts.mode ?? cfg.mode ?? "ensure";
131
+ const prune = opts.prune ?? cfg.prune ?? false;
132
+ const dryRun = opts.dryRun ?? cfg.dryRun ?? false;
133
+ // Strict mode always throws; otherwise honor the configured/overridden policy.
134
+ const onError = mode === "strict" ? "throw" : opts.onError ?? cfg.onError ?? "warn";
135
+ const registry = getFlongoIndexRegistry();
136
+ const reports = [];
137
+ const raise = (status, collection, name, message) => {
138
+ if (onError === "throw") {
139
+ throw new Error(`[flongo] Index sync ${status} for ${collection}.${name}: ${message}`);
140
+ }
141
+ console.warn(`[flongo] Index sync ${status} for ${collection}.${name}: ${message}`);
142
+ };
143
+ for (const [collectionName, specs] of Object.entries(registry)) {
144
+ const collection = flongo_1.flongoDb.collection(collectionName);
145
+ // Snapshot the pre-existing indexes once: used both to classify
146
+ // created-vs-exists and to decide pruning (so we never prune what we just
147
+ // created this run).
148
+ const existing = await fetchExistingIndexes(collection);
149
+ const existingNames = new Set(existing.map((i) => i.name));
150
+ for (const spec of specs) {
151
+ const resolvedName = resolveIndexName(spec);
152
+ const preExists = existingNames.has(resolvedName);
153
+ try {
154
+ const createdName = await collection.createIndex(spec.keys, (spec.options ?? {}));
155
+ reports.push({
156
+ collection: collectionName,
157
+ name: createdName ?? resolvedName,
158
+ status: preExists ? "exists" : "created"
159
+ });
160
+ }
161
+ catch (err) {
162
+ const conflict = isConflictError(err);
163
+ const status = conflict ? "conflict" : "failed";
164
+ const message = conflict
165
+ ? err?.message ?? "index options conflict"
166
+ : describeFailure(err);
167
+ reports.push({ collection: collectionName, name: resolvedName, status, error: message });
168
+ raise(status, collectionName, resolvedName, message);
169
+ }
170
+ }
171
+ if (prune) {
172
+ const declaredNames = new Set(specs.map((s) => resolveIndexName(s)));
173
+ for (const idx of existing) {
174
+ const name = idx.name;
175
+ // Never drop the mandatory _id index, and never drop a declared one.
176
+ if (name === "_id_" || declaredNames.has(name)) {
177
+ continue;
178
+ }
179
+ if (dryRun) {
180
+ console.warn(`[flongo] [dry-run] would prune index ${collectionName}.${name}`);
181
+ continue;
182
+ }
183
+ try {
184
+ await collection.dropIndex(name);
185
+ reports.push({ collection: collectionName, name, status: "pruned" });
186
+ console.warn(`[flongo] Pruned out-of-registry index ${collectionName}.${name}`);
187
+ }
188
+ catch (err) {
189
+ const message = describeFailure(err);
190
+ reports.push({ collection: collectionName, name, status: "failed", error: message });
191
+ raise("failed", collectionName, name, message);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ logSummary(reports);
197
+ return reports;
198
+ }
199
+ /**
200
+ * Boot-time entry point invoked by `connectFlongo` after the connection is
201
+ * established. Respects `indexSync.mode` (`"off"` does nothing) and
202
+ * `indexSync.background` (kick off async and log on completion rather than
203
+ * blocking boot). A no-op when no indexes are registered.
204
+ */
205
+ async function runBootIndexSync() {
206
+ const cfg = getIndexSyncConfig();
207
+ const mode = cfg.mode ?? "ensure";
208
+ if (mode === "off") {
209
+ return;
210
+ }
211
+ if (Object.keys(getFlongoIndexRegistry()).length === 0) {
212
+ return;
213
+ }
214
+ if (cfg.background) {
215
+ // Don't block boot; report the outcome when the builds finish. Errors are
216
+ // logged rather than propagated because there is no boot to fail here.
217
+ void syncFlongoIndexes()
218
+ .then((reports) => {
219
+ const failures = reports.filter((r) => r.status === "failed" || r.status === "conflict");
220
+ if (failures.length) {
221
+ console.warn(`[flongo] Background index sync finished with ${failures.length} problem(s).`);
222
+ }
223
+ })
224
+ .catch((err) => {
225
+ console.error("[flongo] Background index sync failed:", err);
226
+ });
227
+ return;
228
+ }
229
+ await syncFlongoIndexes();
230
+ }
231
+ //# sourceMappingURL=indexes.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flongo",
3
- "version": "1.9.0",
3
+ "version": "2.0.0",
4
4
  "description": "Firestore-like fluent query API for MongoDB",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",