flongo 1.8.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 +207 -3
- package/dist/flongo.d.ts +10 -0
- package/dist/flongo.js +8 -0
- package/dist/flongoCollection.d.ts +55 -2
- package/dist/flongoCollection.js +117 -1
- package/dist/flongoQuery.d.ts +75 -0
- package/dist/flongoQuery.js +111 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -1
- package/dist/indexes.d.ts +164 -0
- package/dist/indexes.js +231 -0
- package/package.json +1 -1
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
|
|
|
@@ -118,6 +119,55 @@ const page = await collection.getAll(
|
|
|
118
119
|
);
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
#### Seeded random sort (`orderByRandom`)
|
|
123
|
+
|
|
124
|
+
`orderByRandom(seed)` adds a **deterministic, seeded shuffle** for fair, rotating
|
|
125
|
+
list/browse orderings (e.g. a public directory with no meaningful ranking yet).
|
|
126
|
+
Given the same seed it produces the **same order on every call**, so `$skip`/`$limit`
|
|
127
|
+
pagination stays gapless and non-overlapping across pages; change the seed and the
|
|
128
|
+
whole set reshuffles.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
132
|
+
// Caller owns the rotation policy. Here: a new shuffle each day. Pin the seed for
|
|
133
|
+
// a browsing session (capture on first load, echo back per page) so paging is stable
|
|
134
|
+
// and rotation only affects new sessions.
|
|
135
|
+
const seed = sessionSeed ?? Math.floor(Date.now() / DAY_MS);
|
|
136
|
+
|
|
137
|
+
const query = new FlongoQuery()
|
|
138
|
+
.where('enable').eq(true)
|
|
139
|
+
.orderBy('featured', SortDirection.Descending) // pinned groups stay on top
|
|
140
|
+
.orderByRandom(seed); // shuffle within each featured tier
|
|
141
|
+
|
|
142
|
+
const page = await stays.getAll(query, { offset: 0, count: 24 }); // stable, fair
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- **Composable** with `orderBy`/`thenBy`: sort keys apply in call order and the
|
|
146
|
+
shuffle slots in at *its* call position. `_id` is always appended as a final
|
|
147
|
+
tiebreaker, so pages are gapless even under hash collisions.
|
|
148
|
+
- **Seed is caller-owned** โ Flongo does not decide rotation cadence. Pass a
|
|
149
|
+
time-bucketed seed for rotation, optionally mixed with a session/user id. Number
|
|
150
|
+
and string seeds are normalized to a string, so `1` and `'1'` are equivalent.
|
|
151
|
+
- **Execution**: a query carrying `orderByRandom` runs via an aggregation pipeline
|
|
152
|
+
(using `$toHashedIndexKey`) instead of `find` โ the `getAll`/`getSome` signatures
|
|
153
|
+
and return types are unchanged. **Requires MongoDB server โฅ 8.0**; on older
|
|
154
|
+
servers `orderByRandom` fails fast with an actionable error.
|
|
155
|
+
|
|
156
|
+
#### Raw aggregation escape hatch
|
|
157
|
+
|
|
158
|
+
For computed-field sorts/rankings the fluent builder doesn't cover yet,
|
|
159
|
+
`FlongoCollection.aggregate(pipeline)` runs a raw MongoDB aggregation pipeline and
|
|
160
|
+
returns documents in Entity form (`_id` normalized to a string):
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const topRated = await products.aggregate([
|
|
164
|
+
{ $match: { inStock: true } },
|
|
165
|
+
{ $addFields: { score: { $multiply: ['$rating', '$reviewCount'] } } },
|
|
166
|
+
{ $sort: { score: -1 } },
|
|
167
|
+
{ $limit: 10 }
|
|
168
|
+
]);
|
|
169
|
+
```
|
|
170
|
+
|
|
121
171
|
### Collection Operations
|
|
122
172
|
|
|
123
173
|
```typescript
|
|
@@ -155,18 +205,172 @@ await collection.batchCreate([user1, user2, user3]);
|
|
|
155
205
|
### Configuration
|
|
156
206
|
|
|
157
207
|
```typescript
|
|
158
|
-
//
|
|
208
|
+
// Audit logging is OFF by default โ opt in per collection
|
|
159
209
|
const collection = new FlongoCollection<User>('users', {
|
|
160
|
-
enableEventLogging:
|
|
210
|
+
enableEventLogging: true
|
|
161
211
|
});
|
|
162
212
|
|
|
163
|
-
//
|
|
213
|
+
// Opt in with a custom events collection
|
|
164
214
|
const collection = new FlongoCollection<User>('users', {
|
|
165
215
|
enableEventLogging: true,
|
|
166
216
|
eventsCollectionName: 'audit_logs'
|
|
167
217
|
});
|
|
168
218
|
```
|
|
169
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
|
+
|
|
170
374
|
## Examples
|
|
171
375
|
|
|
172
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,12 +1,27 @@
|
|
|
1
1
|
import { FlongoQuery } from "./flongoQuery";
|
|
2
2
|
import { Entity, Event, Pagination, Repository } from "./types";
|
|
3
|
+
import { Document, IndexDescription } from "mongodb";
|
|
4
|
+
/**
|
|
5
|
+
* Clears the memoized per-connection server-version check. Exported for tests;
|
|
6
|
+
* production code relies on the connection-keyed cache invalidating itself.
|
|
7
|
+
*/
|
|
8
|
+
export declare function __resetServerVersionCache(): void;
|
|
3
9
|
/**
|
|
4
10
|
* Configuration options for FlongoCollection instances
|
|
5
11
|
*/
|
|
6
12
|
export interface FlongoCollectionOptions {
|
|
7
|
-
/**
|
|
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
|
+
*/
|
|
8
18
|
enableEventLogging?: boolean;
|
|
9
|
-
/**
|
|
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
|
+
*/
|
|
10
25
|
eventsCollectionName?: string;
|
|
11
26
|
}
|
|
12
27
|
/**
|
|
@@ -76,6 +91,29 @@ export declare class FlongoCollection<T> {
|
|
|
76
91
|
* @returns Promise resolving to array of documents
|
|
77
92
|
*/
|
|
78
93
|
getAll(query?: FlongoQuery, pagination?: Pagination): Promise<(Entity & T)[]>;
|
|
94
|
+
/**
|
|
95
|
+
* Executes a query carrying an `orderByRandom` via an aggregation pipeline.
|
|
96
|
+
* Verifies the server supports `$toHashedIndexKey` (MongoDB >= 8.0) first so
|
|
97
|
+
* callers get an actionable error rather than a raw driver failure.
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
private getAllRandom;
|
|
101
|
+
/**
|
|
102
|
+
* Runs a raw aggregation pipeline against this collection and returns the
|
|
103
|
+
* documents with their `_id` normalized to a string (Entity form). A low-level
|
|
104
|
+
* escape hatch for computed-field sorts/rankings that the fluent builder does
|
|
105
|
+
* not yet cover; `orderByRandom` is built on the same execution path.
|
|
106
|
+
* @param pipeline - MongoDB aggregation pipeline stages
|
|
107
|
+
* @returns Promise resolving to the aggregated documents in Entity form
|
|
108
|
+
*/
|
|
109
|
+
aggregate(pipeline: Document[]): Promise<(Entity & T)[]>;
|
|
110
|
+
/**
|
|
111
|
+
* Throws a clear, actionable error when the connected MongoDB server predates
|
|
112
|
+
* the `$toHashedIndexKey` operator that `orderByRandom` relies on (added in
|
|
113
|
+
* 8.0), instead of letting an unrecognized-operator driver error surface.
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
private assertRandomSortSupported;
|
|
79
117
|
/**
|
|
80
118
|
* Retrieves a subset of documents with required query and pagination
|
|
81
119
|
* Similar to getAll but requires both query and pagination parameters
|
|
@@ -205,6 +243,21 @@ export declare class FlongoCollection<T> {
|
|
|
205
243
|
* @param clientId - Optional client ID for audit trail
|
|
206
244
|
*/
|
|
207
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>;
|
|
208
261
|
/**
|
|
209
262
|
* Logs an event to the events collection for audit trails
|
|
210
263
|
* Events are only logged if event logging is enabled in options
|
package/dist/flongoCollection.js
CHANGED
|
@@ -1,11 +1,51 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FlongoCollection = void 0;
|
|
4
|
+
exports.__resetServerVersionCache = __resetServerVersionCache;
|
|
4
5
|
const flongo_1 = require("./flongo");
|
|
5
6
|
const flongoQuery_1 = require("./flongoQuery");
|
|
6
7
|
const types_1 = require("./types");
|
|
7
8
|
const mongodb_1 = require("mongodb");
|
|
8
9
|
const errors_1 = require("./errors");
|
|
10
|
+
/**
|
|
11
|
+
* Memoized MongoDB major version for the active `flongoDb` connection. Keyed by
|
|
12
|
+
* the `flongoDb` reference so a reconnect (which replaces `flongoDb`) misses the
|
|
13
|
+
* cache and transparently re-checks; shared across all FlongoCollection
|
|
14
|
+
* instances on the same connection so `orderByRandom` costs at most one
|
|
15
|
+
* `buildInfo` round-trip per connection.
|
|
16
|
+
*/
|
|
17
|
+
let cachedServerMajor;
|
|
18
|
+
/**
|
|
19
|
+
* Resolves the connected server's major version, memoized per connection.
|
|
20
|
+
* Concurrent callers share a single in-flight `buildInfo`; a failed check is not
|
|
21
|
+
* cached so the next call retries.
|
|
22
|
+
*/
|
|
23
|
+
function getServerMajorVersion() {
|
|
24
|
+
if (cachedServerMajor && cachedServerMajor.db === flongo_1.flongoDb) {
|
|
25
|
+
return cachedServerMajor.promise;
|
|
26
|
+
}
|
|
27
|
+
const promise = (async () => {
|
|
28
|
+
const info = await flongo_1.flongoDb.command({ buildInfo: 1 });
|
|
29
|
+
return Array.isArray(info.versionArray)
|
|
30
|
+
? Number(info.versionArray[0])
|
|
31
|
+
: parseInt(String(info.version), 10);
|
|
32
|
+
})();
|
|
33
|
+
// Don't cache a transient failure โ allow the next call to retry.
|
|
34
|
+
promise.catch(() => {
|
|
35
|
+
if (cachedServerMajor?.promise === promise) {
|
|
36
|
+
cachedServerMajor = undefined;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
cachedServerMajor = { db: flongo_1.flongoDb, promise };
|
|
40
|
+
return promise;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Clears the memoized per-connection server-version check. Exported for tests;
|
|
44
|
+
* production code relies on the connection-keyed cache invalidating itself.
|
|
45
|
+
*/
|
|
46
|
+
function __resetServerVersionCache() {
|
|
47
|
+
cachedServerMajor = undefined;
|
|
48
|
+
}
|
|
9
49
|
/**
|
|
10
50
|
* FlongoCollection provides a high-level interface for MongoDB collection operations
|
|
11
51
|
* with automatic entity management, event logging, and fluent query support.
|
|
@@ -44,7 +84,7 @@ class FlongoCollection {
|
|
|
44
84
|
*/
|
|
45
85
|
constructor(collectionName, options = {}) {
|
|
46
86
|
this.collection = flongo_1.flongoDb.collection(collectionName);
|
|
47
|
-
this.options = { enableEventLogging:
|
|
87
|
+
this.options = { enableEventLogging: false, eventsCollectionName: "events", ...options };
|
|
48
88
|
// Initialize events collection only if logging is enabled
|
|
49
89
|
this.events = this.options.enableEventLogging
|
|
50
90
|
? flongo_1.flongoDb.collection(this.options.eventsCollectionName)
|
|
@@ -88,6 +128,11 @@ class FlongoCollection {
|
|
|
88
128
|
* @returns Promise resolving to array of documents
|
|
89
129
|
*/
|
|
90
130
|
async getAll(query, pagination) {
|
|
131
|
+
// A seeded random sort can't be expressed with find().sort() โ it sorts by a
|
|
132
|
+
// computed per-document value, so it routes through an aggregation pipeline.
|
|
133
|
+
if (query?.hasRandomSort()) {
|
|
134
|
+
return this.getAllRandom(query, pagination);
|
|
135
|
+
}
|
|
91
136
|
const mongodbQuery = query?.build() ?? {};
|
|
92
137
|
const mongodbOptions = query?.buildOptions(pagination) ?? new flongoQuery_1.FlongoQuery().buildOptions(pagination);
|
|
93
138
|
try {
|
|
@@ -98,6 +143,49 @@ class FlongoCollection {
|
|
|
98
143
|
throw err;
|
|
99
144
|
}
|
|
100
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Executes a query carrying an `orderByRandom` via an aggregation pipeline.
|
|
148
|
+
* Verifies the server supports `$toHashedIndexKey` (MongoDB >= 8.0) first so
|
|
149
|
+
* callers get an actionable error rather than a raw driver failure.
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
async getAllRandom(query, pagination) {
|
|
153
|
+
await this.assertRandomSortSupported();
|
|
154
|
+
const pipeline = query.buildPipeline(pagination);
|
|
155
|
+
const res = await this.collection.aggregate(pipeline).toArray();
|
|
156
|
+
return res.map((d) => this.toEntity(d));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Runs a raw aggregation pipeline against this collection and returns the
|
|
160
|
+
* documents with their `_id` normalized to a string (Entity form). A low-level
|
|
161
|
+
* escape hatch for computed-field sorts/rankings that the fluent builder does
|
|
162
|
+
* not yet cover; `orderByRandom` is built on the same execution path.
|
|
163
|
+
* @param pipeline - MongoDB aggregation pipeline stages
|
|
164
|
+
* @returns Promise resolving to the aggregated documents in Entity form
|
|
165
|
+
*/
|
|
166
|
+
async aggregate(pipeline) {
|
|
167
|
+
const res = await this.collection.aggregate(pipeline).toArray();
|
|
168
|
+
return res.map((d) => this.toEntity(d));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Throws a clear, actionable error when the connected MongoDB server predates
|
|
172
|
+
* the `$toHashedIndexKey` operator that `orderByRandom` relies on (added in
|
|
173
|
+
* 8.0), instead of letting an unrecognized-operator driver error surface.
|
|
174
|
+
* @private
|
|
175
|
+
*/
|
|
176
|
+
async assertRandomSortSupported() {
|
|
177
|
+
let major;
|
|
178
|
+
try {
|
|
179
|
+
major = await getServerMajorVersion();
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.error("Failed to verify MongoDB server version for orderByRandom: ", err);
|
|
183
|
+
throw new errors_1.Error400("orderByRandom() could not verify the MongoDB server version. It requires MongoDB server >= 8.0 (uses $toHashedIndexKey).");
|
|
184
|
+
}
|
|
185
|
+
if (!(major >= 8)) {
|
|
186
|
+
throw new errors_1.Error400(`orderByRandom() requires MongoDB server >= 8.0 (uses $toHashedIndexKey), but the connected server is ${major}.x.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
101
189
|
/**
|
|
102
190
|
* Retrieves a subset of documents with required query and pagination
|
|
103
191
|
* Similar to getAll but requires both query and pagination parameters
|
|
@@ -106,6 +194,10 @@ class FlongoCollection {
|
|
|
106
194
|
* @returns Promise resolving to array of documents
|
|
107
195
|
*/
|
|
108
196
|
async getSome(query, pagination) {
|
|
197
|
+
// Mirror getAll: a seeded random sort routes through the aggregation path.
|
|
198
|
+
if (query?.hasRandomSort()) {
|
|
199
|
+
return this.getAllRandom(query, pagination);
|
|
200
|
+
}
|
|
109
201
|
const mongodbQuery = query?.build();
|
|
110
202
|
const mongodbOptions = query?.buildOptions(pagination);
|
|
111
203
|
return (await this.collection.find(mongodbQuery, mongodbOptions).toArray()).map((d) => this.toEntity(d));
|
|
@@ -423,6 +515,30 @@ class FlongoCollection {
|
|
|
423
515
|
});
|
|
424
516
|
}
|
|
425
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
|
+
// ===========================================
|
|
426
542
|
// EVENT LOGGING
|
|
427
543
|
// ===========================================
|
|
428
544
|
/**
|
package/dist/flongoQuery.d.ts
CHANGED
|
@@ -24,6 +24,22 @@ export declare class FlongoQuery implements ICollectionQuery {
|
|
|
24
24
|
* entries act as tiebreakers (in order). Populated by `orderBy()`/`thenBy()`.
|
|
25
25
|
*/
|
|
26
26
|
sorts: Sort[];
|
|
27
|
+
/**
|
|
28
|
+
* Normalized seed for a deterministic random sort, set by `orderByRandom()`.
|
|
29
|
+
* `undefined` means the query has no random sort and executes via the normal
|
|
30
|
+
* `find` path. When present, the query must execute via an aggregation
|
|
31
|
+
* pipeline (see `buildPipeline`). Requires MongoDB server >= 8.0.
|
|
32
|
+
*/
|
|
33
|
+
randomSeed?: string;
|
|
34
|
+
/**
|
|
35
|
+
* The position of the random sort within the sort-key order, captured as
|
|
36
|
+
* `sorts.length` at the time `orderByRandom()` was called. Explicit sort keys
|
|
37
|
+
* added before this index sort ahead of the shuffle; those added at/after it
|
|
38
|
+
* sort behind it. `undefined` when there is no random sort.
|
|
39
|
+
*/
|
|
40
|
+
randomSortPosition?: number;
|
|
41
|
+
/** Internal computed field used to carry the per-document shuffle hash. */
|
|
42
|
+
static readonly SHUFFLE_FIELD = "__flongoShuffle";
|
|
27
43
|
/**
|
|
28
44
|
* Primary sort field, derived from `sorts[0]`.
|
|
29
45
|
* @deprecated Retained for backward compatibility; use `sorts`.
|
|
@@ -263,6 +279,43 @@ export declare class FlongoQuery implements ICollectionQuery {
|
|
|
263
279
|
* @returns This query instance for chaining
|
|
264
280
|
*/
|
|
265
281
|
thenBy(field?: string, direction?: SortDirection): FlongoQuery;
|
|
282
|
+
/**
|
|
283
|
+
* Adds a deterministic, seeded random sort. Given the same seed the produced
|
|
284
|
+
* order is identical on every call, so `$skip`/`$limit` pagination stays
|
|
285
|
+
* gapless and non-overlapping across pages; change the seed and the whole set
|
|
286
|
+
* reshuffles. This is what enables fair, rotating list/browse orderings.
|
|
287
|
+
*
|
|
288
|
+
* Composes with `orderBy`/`thenBy`: sort keys apply in call order and the
|
|
289
|
+
* shuffle slots in at the position of *this* call, so
|
|
290
|
+
* `orderBy('featured', Descending).orderByRandom(seed)` pins featured docs to
|
|
291
|
+
* the top and shuffles within each tier. `_id` is always appended as a final
|
|
292
|
+
* tiebreaker so a total order (and therefore stable paging) is guaranteed even
|
|
293
|
+
* under hash collisions. Calling this alone shuffles the whole result set.
|
|
294
|
+
*
|
|
295
|
+
* The seed is caller-owned โ Flongo does not decide rotation policy. Pass e.g.
|
|
296
|
+
* `Math.floor(Date.now() / DAY_MS)` for daily rotation, optionally mixed with a
|
|
297
|
+
* session/user id. Number and string seeds are normalized to a string, so `1`
|
|
298
|
+
* and `'1'` produce the same order.
|
|
299
|
+
*
|
|
300
|
+
* Implemented natively via the `$toHashedIndexKey` aggregation operator, so a
|
|
301
|
+
* query carrying a random sort executes via an aggregation pipeline instead of
|
|
302
|
+
* `find`. **Requires MongoDB server >= 8.0.**
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* new FlongoQuery()
|
|
306
|
+
* .where('enable').eq(true)
|
|
307
|
+
* .orderBy('featured', SortDirection.Descending)
|
|
308
|
+
* .orderByRandom(seed);
|
|
309
|
+
*
|
|
310
|
+
* @param seed - Seed controlling the shuffle (number or string)
|
|
311
|
+
* @returns This query instance for chaining
|
|
312
|
+
*/
|
|
313
|
+
orderByRandom(seed: number | string): FlongoQuery;
|
|
314
|
+
/**
|
|
315
|
+
* Whether this query carries a random sort and must execute via an aggregation
|
|
316
|
+
* pipeline (`buildPipeline`) rather than the normal `find` path.
|
|
317
|
+
*/
|
|
318
|
+
hasRandomSort(): boolean;
|
|
266
319
|
/**
|
|
267
320
|
* Adds a sub-query with AND logic
|
|
268
321
|
* @param query - Sub-query to combine with AND logic
|
|
@@ -287,6 +340,28 @@ export declare class FlongoQuery implements ICollectionQuery {
|
|
|
287
340
|
* @returns MongoDB FindOptions object
|
|
288
341
|
*/
|
|
289
342
|
buildOptions<T extends Document>(pagination?: Pagination): FindOptions<T>;
|
|
343
|
+
/**
|
|
344
|
+
* Builds the ordered `$sort` specification for the aggregation path, weaving
|
|
345
|
+
* the internal shuffle field in at its call position amongst the explicit
|
|
346
|
+
* sort keys and always appending `_id` as a final tiebreaker. Only used when
|
|
347
|
+
* a random sort is present.
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
private buildAggregationSort;
|
|
351
|
+
/**
|
|
352
|
+
* Compiles this query into an aggregation pipeline that materializes the same
|
|
353
|
+
* results as `find` would, but ordered by a deterministic per-document shuffle
|
|
354
|
+
* (see `orderByRandom`). The `$match` stage is exactly what `find` receives
|
|
355
|
+
* today (`build()`), so all filter logic and indexes carry over unchanged;
|
|
356
|
+
* `$skip`/`$limit` are applied *after* the deterministic sort, which is what
|
|
357
|
+
* makes paging stable.
|
|
358
|
+
*
|
|
359
|
+
* **Requires MongoDB server >= 8.0** (uses `$toHashedIndexKey`).
|
|
360
|
+
*
|
|
361
|
+
* @param pagination - Optional pagination settings
|
|
362
|
+
* @returns MongoDB aggregation pipeline stages
|
|
363
|
+
*/
|
|
364
|
+
buildPipeline<T>(pagination?: Pagination): Document[];
|
|
290
365
|
}
|
|
291
366
|
/**
|
|
292
367
|
* FlongoQueryBuilder provides a builder pattern wrapper around FlongoQuery
|
package/dist/flongoQuery.js
CHANGED
|
@@ -414,6 +414,49 @@ class FlongoQuery {
|
|
|
414
414
|
}
|
|
415
415
|
return this;
|
|
416
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Adds a deterministic, seeded random sort. Given the same seed the produced
|
|
419
|
+
* order is identical on every call, so `$skip`/`$limit` pagination stays
|
|
420
|
+
* gapless and non-overlapping across pages; change the seed and the whole set
|
|
421
|
+
* reshuffles. This is what enables fair, rotating list/browse orderings.
|
|
422
|
+
*
|
|
423
|
+
* Composes with `orderBy`/`thenBy`: sort keys apply in call order and the
|
|
424
|
+
* shuffle slots in at the position of *this* call, so
|
|
425
|
+
* `orderBy('featured', Descending).orderByRandom(seed)` pins featured docs to
|
|
426
|
+
* the top and shuffles within each tier. `_id` is always appended as a final
|
|
427
|
+
* tiebreaker so a total order (and therefore stable paging) is guaranteed even
|
|
428
|
+
* under hash collisions. Calling this alone shuffles the whole result set.
|
|
429
|
+
*
|
|
430
|
+
* The seed is caller-owned โ Flongo does not decide rotation policy. Pass e.g.
|
|
431
|
+
* `Math.floor(Date.now() / DAY_MS)` for daily rotation, optionally mixed with a
|
|
432
|
+
* session/user id. Number and string seeds are normalized to a string, so `1`
|
|
433
|
+
* and `'1'` produce the same order.
|
|
434
|
+
*
|
|
435
|
+
* Implemented natively via the `$toHashedIndexKey` aggregation operator, so a
|
|
436
|
+
* query carrying a random sort executes via an aggregation pipeline instead of
|
|
437
|
+
* `find`. **Requires MongoDB server >= 8.0.**
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* new FlongoQuery()
|
|
441
|
+
* .where('enable').eq(true)
|
|
442
|
+
* .orderBy('featured', SortDirection.Descending)
|
|
443
|
+
* .orderByRandom(seed);
|
|
444
|
+
*
|
|
445
|
+
* @param seed - Seed controlling the shuffle (number or string)
|
|
446
|
+
* @returns This query instance for chaining
|
|
447
|
+
*/
|
|
448
|
+
orderByRandom(seed) {
|
|
449
|
+
this.randomSeed = String(seed);
|
|
450
|
+
this.randomSortPosition = this.sorts.length;
|
|
451
|
+
return this;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Whether this query carries a random sort and must execute via an aggregation
|
|
455
|
+
* pipeline (`buildPipeline`) rather than the normal `find` path.
|
|
456
|
+
*/
|
|
457
|
+
hasRandomSort() {
|
|
458
|
+
return this.randomSeed !== undefined;
|
|
459
|
+
}
|
|
417
460
|
// ===========================================
|
|
418
461
|
// LOGICAL OPERATORS
|
|
419
462
|
// ===========================================
|
|
@@ -552,8 +595,76 @@ class FlongoQuery {
|
|
|
552
595
|
}
|
|
553
596
|
return mongodbOptions;
|
|
554
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Builds the ordered `$sort` specification for the aggregation path, weaving
|
|
600
|
+
* the internal shuffle field in at its call position amongst the explicit
|
|
601
|
+
* sort keys and always appending `_id` as a final tiebreaker. Only used when
|
|
602
|
+
* a random sort is present.
|
|
603
|
+
* @private
|
|
604
|
+
*/
|
|
605
|
+
buildAggregationSort() {
|
|
606
|
+
const sort = {};
|
|
607
|
+
const addShuffle = () => {
|
|
608
|
+
sort[FlongoQuery.SHUFFLE_FIELD] = 1;
|
|
609
|
+
};
|
|
610
|
+
// Iterate the raw sort keys so the random slot lands at the exact index it
|
|
611
|
+
// was declared at (positions are captured against the unfiltered array).
|
|
612
|
+
this.sorts.forEach((s, i) => {
|
|
613
|
+
if (i === this.randomSortPosition) {
|
|
614
|
+
addShuffle();
|
|
615
|
+
}
|
|
616
|
+
if (s.field && s.direction) {
|
|
617
|
+
sort[s.field] = s.direction === types_1.SortDirection.Ascending ? 1 : -1;
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
// Random sort declared at or after the last explicit key.
|
|
621
|
+
if (this.randomSortPosition !== undefined && this.randomSortPosition >= this.sorts.length) {
|
|
622
|
+
addShuffle();
|
|
623
|
+
}
|
|
624
|
+
// Final tiebreaker guarantees a total order (gapless pages under collisions).
|
|
625
|
+
if (!("_id" in sort)) {
|
|
626
|
+
sort["_id"] = 1;
|
|
627
|
+
}
|
|
628
|
+
return sort;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Compiles this query into an aggregation pipeline that materializes the same
|
|
632
|
+
* results as `find` would, but ordered by a deterministic per-document shuffle
|
|
633
|
+
* (see `orderByRandom`). The `$match` stage is exactly what `find` receives
|
|
634
|
+
* today (`build()`), so all filter logic and indexes carry over unchanged;
|
|
635
|
+
* `$skip`/`$limit` are applied *after* the deterministic sort, which is what
|
|
636
|
+
* makes paging stable.
|
|
637
|
+
*
|
|
638
|
+
* **Requires MongoDB server >= 8.0** (uses `$toHashedIndexKey`).
|
|
639
|
+
*
|
|
640
|
+
* @param pagination - Optional pagination settings
|
|
641
|
+
* @returns MongoDB aggregation pipeline stages
|
|
642
|
+
*/
|
|
643
|
+
buildPipeline(pagination) {
|
|
644
|
+
const pipeline = [{ $match: this.build() }];
|
|
645
|
+
// Hash the full `_id` string mixed with the seed. Hashing the whole _id
|
|
646
|
+
// decorrelates the order from the ObjectId timestamp prefix, so newer docs
|
|
647
|
+
// aren't biased toward one end of the shuffle.
|
|
648
|
+
pipeline.push({
|
|
649
|
+
$addFields: {
|
|
650
|
+
[FlongoQuery.SHUFFLE_FIELD]: {
|
|
651
|
+
$toHashedIndexKey: { $concat: [{ $toString: "$_id" }, ":", this.randomSeed] }
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
pipeline.push({ $sort: this.buildAggregationSort() });
|
|
656
|
+
if (pagination) {
|
|
657
|
+
pipeline.push({ $skip: pagination.offset });
|
|
658
|
+
pipeline.push({ $limit: pagination.count });
|
|
659
|
+
}
|
|
660
|
+
// Don't leak the internal shuffle field to callers.
|
|
661
|
+
pipeline.push({ $project: { [FlongoQuery.SHUFFLE_FIELD]: 0 } });
|
|
662
|
+
return pipeline;
|
|
663
|
+
}
|
|
555
664
|
}
|
|
556
665
|
exports.FlongoQuery = FlongoQuery;
|
|
666
|
+
/** Internal computed field used to carry the per-document shuffle hash. */
|
|
667
|
+
FlongoQuery.SHUFFLE_FIELD = "__flongoShuffle";
|
|
557
668
|
/**
|
|
558
669
|
* FlongoQueryBuilder provides a builder pattern wrapper around FlongoQuery
|
|
559
670
|
* This can be useful for more complex query construction scenarios
|
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
|
package/dist/indexes.js
ADDED
|
@@ -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
|