monsqlize 2.0.3 → 2.0.4

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/CHANGELOG.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # CHANGELOG
2
2
 
3
- > Summary index — current release details are packaged in [changelogs/v2.0.3.md](./changelogs/v2.0.3.md); historical details live in the repository changelog archive.
4
- > **Last updated**: 2026-06-11
3
+ > Summary index — current release details are packaged in [changelogs/v2.0.4.md](./changelogs/v2.0.4.md); historical details live in the repository changelog archive.
4
+ > **Last updated**: 2026-06-12
5
5
 
6
6
  ---
7
7
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  | Version | Date | Summary | Details |
11
11
  |---------|------|---------|---------|
12
+ | [v2.0.4](./changelogs/v2.0.4.md) | 2026-06-12 | Patch: production-safe Model index rollout controls, `schema-dsl@2.0.9`, capability-index wording cleanup, and documentation home refinements | [View](./changelogs/v2.0.4.md) |
12
13
  | [v2.0.3](./changelogs/v2.0.3.md) | 2026-06-11 | Patch: v1 compatibility fixes, public stats APIs, standalone docs-site link safety, bilingual docs consistency, and release preflight alignment | [View](./changelogs/v2.0.3.md) |
13
14
  | [v2.0.2](./changelogs/v2.0.2.md) | 2026-06-09 | Patch: direct runtime, optional and development dependencies pinned to exact versions for deterministic consumer installs | [View](./changelogs/v2.0.2.md) |
14
15
  | [v2.0.1](./changelogs/v2.0.1.md) | 2026-06-03 | Patch: model collection/pool compatibility, automatic-index task dedupe, runtime cache/pool v1 smooth-upgrade fixes, and current docs/types alignment | [View](./changelogs/v2.0.1.md) |
@@ -446,7 +447,8 @@ const result = await msq.collection('orders').insertOne(dataFromMongoose);
446
447
  changelogs/
447
448
  ├── README.md # 变更文档说明
448
449
  ├── TEMPLATE.md # 变更文档模板
449
- ├── v2.0.3.md # 当前发布详细变更
450
+ ├── v2.0.4.md # 当前发布详细变更
451
+ ├── v2.0.3.md # v2.0.3 详细变更
450
452
  ├── v2.0.2.md # v2.0.2 详细变更
451
453
  ├── v2.0.1.md # v2.0.1 详细变更
452
454
  ├── v2.0.0.md # v2 TypeScript 重写发布详细变更
@@ -500,7 +502,8 @@ changelogs/
500
502
 
501
503
  ## 相关文档
502
504
 
503
- - [changelogs/v2.0.3.md](./changelogs/v2.0.3.md) - 当前发布详细变更文档
505
+ - [changelogs/v2.0.4.md](./changelogs/v2.0.4.md) - 当前发布详细变更文档
506
+ - [changelogs/v2.0.3.md](./changelogs/v2.0.3.md) - v2.0.3 详细变更文档
504
507
  - [changelogs/v2.0.2.md](./changelogs/v2.0.2.md) - v2.0.2 详细变更文档
505
508
  - [changelogs/v2.0.1.md](./changelogs/v2.0.1.md) - v2.0.1 详细变更文档
506
509
  - [README.md](./README.md) - 项目说明
@@ -508,5 +511,5 @@ changelogs/
508
511
 
509
512
  ---
510
513
 
511
- **最后更新**: 2026-06-11
514
+ **最后更新**: 2026-06-12
512
515
 
package/README.md CHANGED
@@ -7,6 +7,8 @@ TypeScript-native MongoDB ODM and enhancement layer with v1-compatible APIs, mul
7
7
  [![MongoDB](https://img.shields.io/badge/MongoDB-6.x%20%2F%207.x-green.svg)](https://www.mongodb.com/)
8
8
  [![Node.js](https://img.shields.io/badge/Node.js-18%2B-brightgreen)](https://nodejs.org/)
9
9
 
10
+ Documentation: [English](https://vextjs.github.io/monSQLize/) · [简体中文](https://vextjs.github.io/monSQLize/zh/)
11
+
10
12
  ```bash
11
13
  npm install monsqlize
12
14
  ```
@@ -37,7 +39,7 @@ monSQLize keeps the MongoDB driver mental model while adding the production feat
37
39
 
38
40
  - Drop-in collection helpers that preserve MongoDB-style CRUD, aggregation, indexes, transactions, and Change Streams.
39
41
  - Smart caching through `cache-hub`, including local memory caching, optional Redis-backed L2 caching, automatic invalidation, and function-level caching.
40
- - A lightweight Model layer with `schema-dsl` validation, hooks, relations, populate, custom methods, timestamps, soft delete, and optimistic locking.
42
+ - A lightweight Model layer with `schema-dsl` validation, hooks, relations, populate, custom methods, timestamps, soft delete, optimistic locking, and production-safe index preflight.
41
43
  - Multi-connection-pool support, pool health checks, pool-scoped collections/models, and fallback strategies.
42
44
  - Business locks and distributed locks for multi-instance deployments.
43
45
  - Saga orchestration for multi-step business workflows.
@@ -251,6 +253,27 @@ module.exports = {
251
253
 
252
254
  Relative model paths are resolved from `process.cwd()`. In production services, prefer absolute paths such as `path.join(__dirname, 'models')`.
253
255
 
256
+ ### Production Model Index Rollout
257
+
258
+ Model-declared indexes are still created automatically by default for backward compatibility. Production services can turn off automatic indexing and run an explicit preflight before creating missing indexes:
259
+
260
+ ```js
261
+ const db = new MonSQLize({
262
+ type: 'mongodb',
263
+ databaseName: 'mydb',
264
+ config: { uri: 'mongodb://localhost:27017' },
265
+ autoIndex: false
266
+ });
267
+
268
+ const plan = await db.ensureModelIndexes({ models: ['users'], dryRun: true });
269
+
270
+ if (plan.totals.conflicts === 0) {
271
+ await db.ensureModelIndexes({ models: ['users'], throwOnError: true });
272
+ }
273
+ ```
274
+
275
+ `ensureModelIndexes()` creates only missing indexes. It does not drop, rename, or rebuild conflicting indexes.
276
+
254
277
  ### Populate
255
278
 
256
279
  ```js
@@ -443,6 +466,7 @@ See the current support and verification documents:
443
466
 
444
467
  Current TypeScript documentation and examples are the source of truth for the v2 package:
445
468
 
469
+ - Complete docs: [English](https://vextjs.github.io/monSQLize/) · [简体中文](https://vextjs.github.io/monSQLize/zh/)
446
470
  - `docs/en/**` - default English documentation.
447
471
  - `docs/zh/**` - Simplified Chinese documentation.
448
472
  - `docs/en/recipes.md` / `docs/zh/recipes.md` - shortest copy-ready paths for common setup scenarios.
@@ -494,7 +518,7 @@ npm run test:real-env:private
494
518
 
495
519
  ## Release Status
496
520
 
497
- The current published release is `v2.0.3`.
521
+ The current published release is `v2.0.4`.
498
522
 
499
523
  Key release-readiness points:
500
524
 
@@ -502,11 +526,17 @@ Key release-readiness points:
502
526
  - Package exports are consolidated under `dist/cjs`, `dist/esm`, and `dist/types`.
503
527
  - npm packages include the runtime bundles and declaration files only; source maps are disabled by default and can be generated locally with `MONSQLIZE_BUILD_SOURCEMAPS=1 npm run build`.
504
528
  - v1 smooth-upgrade compatibility has been validated against the target workspace consumers.
505
- - `schema-dsl` follows the npm `latest` TypeScript line `schema-dsl@2.0.8`; deprecated `2.3.x` mistake releases are intentionally excluded.
529
+ - `schema-dsl` follows the npm `latest` TypeScript line `schema-dsl@2.0.9`; deprecated `2.3.x` mistake releases are intentionally excluded.
506
530
  - GitHub Actions publishes to npm from `v*` tags after running `npm run release:preflight`; the publish step skips duplicate lifecycle scripts because the gate already ran in the same job.
507
531
 
508
532
  ## Roadmap
509
533
 
534
+ ### v2.0.4
535
+
536
+ - Production-safe Model index rollout controls with `autoIndex`, dry-run preflight, conflict reporting, and explicit `ensureIndexes()` / `ensureModelIndexes()` APIs.
537
+ - `schema-dsl` updated to `2.0.9`, with transitive `cache-hub` aligned to `2.2.4`.
538
+ - User-facing capability and verification documentation wording cleaned up, plus documentation home experience refinements.
539
+
510
540
  ### v2.0.3
511
541
 
512
542
  - v1 compatibility patch for documented `findPage({ cache })` behavior.
@@ -0,0 +1,61 @@
1
+ # v2.0.4 — 2026-06-12
2
+
3
+ ## Overview
4
+
5
+ v2.0.4 is a production-safety and documentation-quality patch. It keeps the public API backward compatible while adding explicit Model index rollout controls, updating the schema validation dependency, and refining user-facing documentation language.
6
+
7
+ ---
8
+
9
+ ## Runtime Fixes
10
+
11
+ - Added production-safe Model index controls:
12
+ - runtime-level and model-level `autoIndex` options.
13
+ - explicit `ModelInstance.ensureIndexes()` and `MonSQLize.ensureModelIndexes()` APIs.
14
+ - dry-run index preflight with `existing`, `missing`, `conflicts`, and `failed` classification.
15
+ - missing-index creation without dropping, renaming, or rebuilding conflicting indexes.
16
+ - `model-index-error` events for observable automatic-index creation failures.
17
+ - Kept automatic Model indexing enabled by default for backward compatibility.
18
+ - Moved runtime Model index aggregation into the Model runtime helper layer so the strict source-size gate remains clean.
19
+
20
+ ## Dependencies
21
+
22
+ - Updated `schema-dsl` from `2.0.8` to npm `latest` `2.0.9`.
23
+ - Confirmed `schema-dsl@2.0.9` keeps the required `dsl()` and `validate()` runtime paths compatible with monSQLize.
24
+ - Confirmed the `schema-dsl` transitive `cache-hub` dependency now resolves to `2.2.4`, matching the root direct dependency.
25
+
26
+ ## Documentation
27
+
28
+ - Documented the recommended production index rollout flow:
29
+ - set `autoIndex: false` in production services.
30
+ - run `ensureIndexes({ dryRun: true })` / `ensureModelIndexes({ dryRun: true })`.
31
+ - create only missing indexes during a low-traffic maintenance window.
32
+ - monitor TTL and index-build behavior.
33
+ - Corrected create-index documentation to distinguish local `INVALID_ARGUMENT` validation from raw MongoDB driver/server index conflicts.
34
+ - Reworded capability-index and verification-entry documentation to use user-facing availability and verification language instead of internal migration status terms.
35
+ - Refined the documentation home experience with the VextJS-style footer, aligned hero/feature layout, and updated hero data-flow visual.
36
+
37
+ ---
38
+
39
+ ## Compatibility
40
+
41
+ - SemVer: patch release.
42
+ - Breaking changes: none.
43
+ - Existing v2 imports remain under `dist/cjs`, `dist/esm`, and `dist/types`.
44
+ - Existing default automatic Model indexing behavior remains enabled unless users opt into the new production-safe controls.
45
+
46
+ ---
47
+
48
+ ## Validation
49
+
50
+ - `npm run lint`
51
+ - `npm run check:docs-examples`
52
+ - `npm run type-check`
53
+ - targeted Model/index unit and integration tests
54
+ - `npm run test:examples`
55
+ - `npm --prefix website run build`
56
+ - `npm run verify:fast`
57
+ - `npm run test:server-matrix`
58
+ - `npm run test:audit`
59
+ - `git diff --check`
60
+
61
+ Full release validation is handled by `npm run release:preflight`, `npm pack --dry-run`, `npm publish --dry-run`, and pack install smoke before publishing.
@@ -997,6 +997,75 @@ function stableIndexStringify(value) {
997
997
  }
998
998
  return JSON.stringify(value) ?? "undefined";
999
999
  }
1000
+ function getIndexOptionName(options) {
1001
+ return typeof options.name === "string" && options.name.length > 0 ? options.name : void 0;
1002
+ }
1003
+ function summarizeIndexError(error) {
1004
+ if (error instanceof Error) {
1005
+ const record = error;
1006
+ return {
1007
+ name: error.name,
1008
+ message: error.message,
1009
+ code: record.code ?? record.codeName
1010
+ };
1011
+ }
1012
+ return {
1013
+ message: String(error)
1014
+ };
1015
+ }
1016
+ function isRecord(value) {
1017
+ return !!value && typeof value === "object" && !Array.isArray(value);
1018
+ }
1019
+ function getExistingIndexKey(index) {
1020
+ return index.key;
1021
+ }
1022
+ function declaredOptionEntries(options) {
1023
+ return Object.entries(options).filter(([name, value]) => {
1024
+ if (value === void 0) return false;
1025
+ if (name === "background") return false;
1026
+ return true;
1027
+ });
1028
+ }
1029
+ function indexOptionsMatch(existing, declared) {
1030
+ if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
1031
+ return false;
1032
+ }
1033
+ for (const [name, value] of declaredOptionEntries(declared.options)) {
1034
+ const existingValue = existing[name];
1035
+ if (value === false && existingValue === void 0) {
1036
+ continue;
1037
+ }
1038
+ if (stableIndexStringify(existingValue) !== stableIndexStringify(value)) {
1039
+ return false;
1040
+ }
1041
+ }
1042
+ return true;
1043
+ }
1044
+ function findExistingIndexByName(existingIndexes, name) {
1045
+ if (!name) return void 0;
1046
+ return existingIndexes.find((index) => index.name === name);
1047
+ }
1048
+ function findExistingIndexByKey(existingIndexes, key) {
1049
+ const fingerprint = stableIndexStringify(key);
1050
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
1051
+ }
1052
+ function createIndexEnsureError(message, result, cause) {
1053
+ return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
1054
+ }
1055
+ function resolveModelAutoIndexOptions(definition, runtimeAutoIndex) {
1056
+ const modelAutoIndex = toCompatDefinition(definition).options?.autoIndex;
1057
+ const value = modelAutoIndex ?? runtimeAutoIndex;
1058
+ if (value === false) {
1059
+ return { enabled: false, emitEvents: true };
1060
+ }
1061
+ if (value && typeof value === "object") {
1062
+ return {
1063
+ enabled: value.enabled !== false,
1064
+ emitEvents: value.emitEvents !== false
1065
+ };
1066
+ }
1067
+ return { enabled: true, emitEvents: true };
1068
+ }
1000
1069
  function getIndexTaskRegistry(runtime) {
1001
1070
  if (!runtime) {
1002
1071
  return fallbackModelIndexTasks;
@@ -1024,6 +1093,20 @@ function resolveIndexTaskScope(collection, options) {
1024
1093
  };
1025
1094
  }
1026
1095
  }
1096
+ function toIndexNamespace(scope) {
1097
+ return {
1098
+ db: scope.dbName,
1099
+ collection: scope.collectionName,
1100
+ poolName: scope.poolName
1101
+ };
1102
+ }
1103
+ function emitIndexFailure(runtime, payload, emitEvents) {
1104
+ if (!emitEvents) {
1105
+ return;
1106
+ }
1107
+ const emitter = runtime;
1108
+ emitter?.emit?.("model-index-error", payload);
1109
+ }
1027
1110
  function warnIndexFailure(runtime, taskKey, error) {
1028
1111
  const logger = runtime;
1029
1112
  logger?.logger?.warn?.("[MonSQLize] model index creation failed", {
@@ -1031,9 +1114,10 @@ function warnIndexFailure(runtime, taskKey, error) {
1031
1114
  error: error instanceof Error ? error.message : String(error)
1032
1115
  });
1033
1116
  }
1034
- function scheduleIndexTask(collection, key, indexOptions, options) {
1117
+ function scheduleIndexTask(collection, declaredIndex, emitEvents, options) {
1035
1118
  const scope = resolveIndexTaskScope(collection, options);
1036
- const indexFingerprint = stableIndexStringify({ key, options: indexOptions });
1119
+ const { key, options: indexOptions } = declaredIndex;
1120
+ const indexFingerprint = declaredIndex.fingerprint;
1037
1121
  const taskKey = `${scope.poolName}:${scope.dbName}:${scope.collectionName}:${indexFingerprint}`;
1038
1122
  const registry = getIndexTaskRegistry(options?.runtime);
1039
1123
  const existing = registry.get(taskKey);
@@ -1051,6 +1135,14 @@ function scheduleIndexTask(collection, key, indexOptions, options) {
1051
1135
  task.status = "failed";
1052
1136
  task.error = error;
1053
1137
  warnIndexFailure(options?.runtime, taskKey, error);
1138
+ emitIndexFailure(options?.runtime, {
1139
+ namespace: scope,
1140
+ taskKey,
1141
+ source: declaredIndex.source,
1142
+ key,
1143
+ options: indexOptions,
1144
+ error: summarizeIndexError(error)
1145
+ }, emitEvents);
1054
1146
  resolve();
1055
1147
  });
1056
1148
  });
@@ -1167,6 +1259,134 @@ function resolveModelHooksFactory(definition) {
1167
1259
  const hooks = toCompatDefinition(definition).hooks;
1168
1260
  return typeof hooks === "function" ? hooks : null;
1169
1261
  }
1262
+ function collectModelIndexDefinitions(definition, softDeleteConfig) {
1263
+ const declared = [];
1264
+ if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1265
+ const key = { [softDeleteConfig.field]: 1 };
1266
+ const options = { expireAfterSeconds: softDeleteConfig.ttl };
1267
+ declared.push({
1268
+ source: "softDelete",
1269
+ key,
1270
+ options,
1271
+ name: getIndexOptionName(options),
1272
+ fingerprint: stableIndexStringify({ key, options })
1273
+ });
1274
+ }
1275
+ const indexes = toCompatDefinition(definition).indexes;
1276
+ if (!Array.isArray(indexes) || indexes.length === 0) {
1277
+ return declared;
1278
+ }
1279
+ for (const indexSpec of indexes) {
1280
+ if (!isRecord(indexSpec) || !indexSpec.key) {
1281
+ continue;
1282
+ }
1283
+ const { key, ...indexOptions } = indexSpec;
1284
+ declared.push({
1285
+ source: "definition",
1286
+ key,
1287
+ options: indexOptions,
1288
+ name: getIndexOptionName(indexOptions),
1289
+ fingerprint: stableIndexStringify({ key, options: indexOptions })
1290
+ });
1291
+ }
1292
+ return declared;
1293
+ }
1294
+ async function ensureModelIndexesForCollection(collection, definition, softDeleteConfig, options = {}) {
1295
+ const namespace = toIndexNamespace(resolveIndexTaskScope(collection, options));
1296
+ const declared = collectModelIndexDefinitions(definition, softDeleteConfig);
1297
+ const existingIndexes = await collection.listIndexes();
1298
+ const existing = [];
1299
+ const missing = [];
1300
+ const conflicts = [];
1301
+ for (const declaredIndex of declared) {
1302
+ const existingByName = findExistingIndexByName(existingIndexes, declaredIndex.name);
1303
+ if (existingByName) {
1304
+ if (indexOptionsMatch(existingByName, declaredIndex)) {
1305
+ existing.push({ declared: declaredIndex, existing: existingByName });
1306
+ } else {
1307
+ conflicts.push({
1308
+ declared: declaredIndex,
1309
+ existing: existingByName,
1310
+ reason: "name-conflict"
1311
+ });
1312
+ }
1313
+ continue;
1314
+ }
1315
+ const existingByKey = findExistingIndexByKey(existingIndexes, declaredIndex.key);
1316
+ if (existingByKey) {
1317
+ if (indexOptionsMatch(existingByKey, declaredIndex)) {
1318
+ existing.push({ declared: declaredIndex, existing: existingByKey });
1319
+ } else {
1320
+ conflicts.push({
1321
+ declared: declaredIndex,
1322
+ existing: existingByKey,
1323
+ reason: "options-conflict"
1324
+ });
1325
+ }
1326
+ continue;
1327
+ }
1328
+ missing.push(declaredIndex);
1329
+ }
1330
+ const result = {
1331
+ dryRun: options.dryRun === true,
1332
+ namespace,
1333
+ declared,
1334
+ existing,
1335
+ missing,
1336
+ created: [],
1337
+ conflicts,
1338
+ failed: [],
1339
+ skipped: options.dryRun === true ? missing.map((declaredIndex) => ({ declared: declaredIndex, reason: "dry-run" })) : conflicts.map((conflict) => ({ declared: conflict.declared, reason: conflict.reason }))
1340
+ };
1341
+ if (conflicts.length > 0 && options.throwOnError) {
1342
+ throw createIndexEnsureError("Model index conflicts detected.", result);
1343
+ }
1344
+ if (options.dryRun === true) {
1345
+ return result;
1346
+ }
1347
+ for (const declaredIndex of missing) {
1348
+ try {
1349
+ const createdName = await collection.createIndex(declaredIndex.key, declaredIndex.options);
1350
+ result.created.push({
1351
+ declared: declaredIndex,
1352
+ name: typeof createdName === "string" ? createdName : void 0,
1353
+ result: createdName
1354
+ });
1355
+ } catch (error) {
1356
+ result.failed.push({
1357
+ declared: declaredIndex,
1358
+ error: summarizeIndexError(error)
1359
+ });
1360
+ if (options.throwOnError) {
1361
+ throw createIndexEnsureError(
1362
+ "Model index creation failed.",
1363
+ result,
1364
+ error instanceof Error ? error : void 0
1365
+ );
1366
+ }
1367
+ }
1368
+ }
1369
+ return result;
1370
+ }
1371
+ function summarizeModelIndexEnsureResults(results) {
1372
+ return results.reduce((totals, result) => ({
1373
+ declared: totals.declared + result.declared.length,
1374
+ existing: totals.existing + result.existing.length,
1375
+ missing: totals.missing + result.missing.length,
1376
+ created: totals.created + result.created.length,
1377
+ conflicts: totals.conflicts + result.conflicts.length,
1378
+ failed: totals.failed + result.failed.length,
1379
+ skipped: totals.skipped + result.skipped.length
1380
+ }), {
1381
+ declared: 0,
1382
+ existing: 0,
1383
+ missing: 0,
1384
+ created: 0,
1385
+ conflicts: 0,
1386
+ failed: 0,
1387
+ skipped: 0
1388
+ });
1389
+ }
1170
1390
  function initializeModelV1Methods(target, definition) {
1171
1391
  const methods = toCompatDefinition(definition).methods;
1172
1392
  if (typeof methods !== "function") {
@@ -1191,25 +1411,12 @@ function initializeModelV1Methods(target, definition) {
1191
1411
  }
1192
1412
  }
1193
1413
  function scheduleModelIndexes(collection, definition, softDeleteConfig, options) {
1194
- if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1195
- const softDeleteIndex = softDeleteConfig;
1196
- scheduleIndexTask(
1197
- collection,
1198
- { [softDeleteIndex.field]: 1 },
1199
- { expireAfterSeconds: softDeleteIndex.ttl },
1200
- options
1201
- );
1202
- }
1203
- const indexes = toCompatDefinition(definition).indexes;
1204
- if (!Array.isArray(indexes) || indexes.length === 0) {
1414
+ const autoIndex = resolveModelAutoIndexOptions(definition, options?.autoIndex);
1415
+ if (!autoIndex.enabled) {
1205
1416
  return;
1206
1417
  }
1207
- for (const indexSpec of indexes) {
1208
- if (!indexSpec?.key) {
1209
- continue;
1210
- }
1211
- const { key, ...indexOptions } = indexSpec;
1212
- scheduleIndexTask(collection, key, indexOptions, options);
1418
+ for (const declaredIndex of collectModelIndexDefinitions(definition, softDeleteConfig)) {
1419
+ scheduleIndexTask(collection, declaredIndex, autoIndex.emitEvents, options);
1213
1420
  }
1214
1421
  }
1215
1422
 
@@ -1777,7 +1984,8 @@ var ModelInstance = class {
1777
1984
  runtime: this.runtime,
1778
1985
  dbName: options.dbName,
1779
1986
  poolName: options.poolName,
1780
- collectionName: options.collectionName
1987
+ collectionName: options.collectionName,
1988
+ autoIndex: this.runtime.options?.autoIndex
1781
1989
  });
1782
1990
  this._v1InstanceMethods = initializeModelV1Methods(this, options.definition);
1783
1991
  }
@@ -1981,6 +2189,15 @@ var ModelInstance = class {
1981
2189
  listIndexes() {
1982
2190
  return this.collection.listIndexes();
1983
2191
  }
2192
+ ensureIndexes(options = {}) {
2193
+ return ensureModelIndexesForCollection(this.collection, this.definition, this._softDeleteConfig, {
2194
+ ...options,
2195
+ runtime: this.runtime,
2196
+ dbName: this.dbName,
2197
+ poolName: this.poolName,
2198
+ collectionName: this.collectionName
2199
+ });
2200
+ }
1984
2201
  dropIndex(name) {
1985
2202
  return this.collection.dropIndex(name);
1986
2203
  }
@@ -8079,6 +8296,26 @@ function createRuntimeModelInstance(host, name, scope) {
8079
8296
  });
8080
8297
  return instance;
8081
8298
  }
8299
+ async function ensureRuntimeModelIndexes(host, options = {}) {
8300
+ const modelNames = options.models ?? Model.list();
8301
+ const models = [];
8302
+ for (const name of modelNames) {
8303
+ const model = host.scopedModel(name, {
8304
+ database: options.database,
8305
+ pool: options.pool
8306
+ });
8307
+ const result = await model.ensureIndexes({
8308
+ dryRun: options.dryRun,
8309
+ throwOnError: options.throwOnError
8310
+ });
8311
+ models.push({ name, result });
8312
+ }
8313
+ return {
8314
+ dryRun: options.dryRun === true,
8315
+ models,
8316
+ totals: summarizeModelIndexEnsureResults(models.map((item) => item.result))
8317
+ };
8318
+ }
8082
8319
 
8083
8320
  // src/entry/runtime-core-hosts.ts
8084
8321
  function resolveAdapterCache(state) {
@@ -10787,7 +11024,8 @@ var MonSQLizeRuntime = class {
10787
11024
  namespace: d.namespace,
10788
11025
  log: d.log,
10789
11026
  countQueue: this.options.countQueue,
10790
- models: this.options.models
11027
+ models: this.options.models,
11028
+ autoIndex: this.options.autoIndex
10791
11029
  };
10792
11030
  }
10793
11031
  async close() {
@@ -10984,6 +11222,10 @@ var MonSQLizeRuntime = class {
10984
11222
  cache.set(name, instance);
10985
11223
  return instance;
10986
11224
  }
11225
+ async ensureModelIndexes(options = {}) {
11226
+ this.ensureConnected();
11227
+ return ensureRuntimeModelIndexes(this, options);
11228
+ }
10987
11229
  // Capability delegation ----------------------------------------------------
10988
11230
  async startSession(options = {}) {
10989
11231
  this.ensureConnected();
@@ -980,6 +980,75 @@ function stableIndexStringify(value) {
980
980
  }
981
981
  return JSON.stringify(value) ?? "undefined";
982
982
  }
983
+ function getIndexOptionName(options) {
984
+ return typeof options.name === "string" && options.name.length > 0 ? options.name : void 0;
985
+ }
986
+ function summarizeIndexError(error) {
987
+ if (error instanceof Error) {
988
+ const record = error;
989
+ return {
990
+ name: error.name,
991
+ message: error.message,
992
+ code: record.code ?? record.codeName
993
+ };
994
+ }
995
+ return {
996
+ message: String(error)
997
+ };
998
+ }
999
+ function isRecord(value) {
1000
+ return !!value && typeof value === "object" && !Array.isArray(value);
1001
+ }
1002
+ function getExistingIndexKey(index) {
1003
+ return index.key;
1004
+ }
1005
+ function declaredOptionEntries(options) {
1006
+ return Object.entries(options).filter(([name, value]) => {
1007
+ if (value === void 0) return false;
1008
+ if (name === "background") return false;
1009
+ return true;
1010
+ });
1011
+ }
1012
+ function indexOptionsMatch(existing, declared) {
1013
+ if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
1014
+ return false;
1015
+ }
1016
+ for (const [name, value] of declaredOptionEntries(declared.options)) {
1017
+ const existingValue = existing[name];
1018
+ if (value === false && existingValue === void 0) {
1019
+ continue;
1020
+ }
1021
+ if (stableIndexStringify(existingValue) !== stableIndexStringify(value)) {
1022
+ return false;
1023
+ }
1024
+ }
1025
+ return true;
1026
+ }
1027
+ function findExistingIndexByName(existingIndexes, name) {
1028
+ if (!name) return void 0;
1029
+ return existingIndexes.find((index) => index.name === name);
1030
+ }
1031
+ function findExistingIndexByKey(existingIndexes, key) {
1032
+ const fingerprint = stableIndexStringify(key);
1033
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
1034
+ }
1035
+ function createIndexEnsureError(message, result, cause) {
1036
+ return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
1037
+ }
1038
+ function resolveModelAutoIndexOptions(definition, runtimeAutoIndex) {
1039
+ const modelAutoIndex = toCompatDefinition(definition).options?.autoIndex;
1040
+ const value = modelAutoIndex ?? runtimeAutoIndex;
1041
+ if (value === false) {
1042
+ return { enabled: false, emitEvents: true };
1043
+ }
1044
+ if (value && typeof value === "object") {
1045
+ return {
1046
+ enabled: value.enabled !== false,
1047
+ emitEvents: value.emitEvents !== false
1048
+ };
1049
+ }
1050
+ return { enabled: true, emitEvents: true };
1051
+ }
983
1052
  function getIndexTaskRegistry(runtime) {
984
1053
  if (!runtime) {
985
1054
  return fallbackModelIndexTasks;
@@ -1007,6 +1076,20 @@ function resolveIndexTaskScope(collection, options) {
1007
1076
  };
1008
1077
  }
1009
1078
  }
1079
+ function toIndexNamespace(scope) {
1080
+ return {
1081
+ db: scope.dbName,
1082
+ collection: scope.collectionName,
1083
+ poolName: scope.poolName
1084
+ };
1085
+ }
1086
+ function emitIndexFailure(runtime, payload, emitEvents) {
1087
+ if (!emitEvents) {
1088
+ return;
1089
+ }
1090
+ const emitter = runtime;
1091
+ emitter?.emit?.("model-index-error", payload);
1092
+ }
1010
1093
  function warnIndexFailure(runtime, taskKey, error) {
1011
1094
  const logger = runtime;
1012
1095
  logger?.logger?.warn?.("[MonSQLize] model index creation failed", {
@@ -1014,9 +1097,10 @@ function warnIndexFailure(runtime, taskKey, error) {
1014
1097
  error: error instanceof Error ? error.message : String(error)
1015
1098
  });
1016
1099
  }
1017
- function scheduleIndexTask(collection, key, indexOptions, options) {
1100
+ function scheduleIndexTask(collection, declaredIndex, emitEvents, options) {
1018
1101
  const scope = resolveIndexTaskScope(collection, options);
1019
- const indexFingerprint = stableIndexStringify({ key, options: indexOptions });
1102
+ const { key, options: indexOptions } = declaredIndex;
1103
+ const indexFingerprint = declaredIndex.fingerprint;
1020
1104
  const taskKey = `${scope.poolName}:${scope.dbName}:${scope.collectionName}:${indexFingerprint}`;
1021
1105
  const registry = getIndexTaskRegistry(options?.runtime);
1022
1106
  const existing = registry.get(taskKey);
@@ -1034,6 +1118,14 @@ function scheduleIndexTask(collection, key, indexOptions, options) {
1034
1118
  task.status = "failed";
1035
1119
  task.error = error;
1036
1120
  warnIndexFailure(options?.runtime, taskKey, error);
1121
+ emitIndexFailure(options?.runtime, {
1122
+ namespace: scope,
1123
+ taskKey,
1124
+ source: declaredIndex.source,
1125
+ key,
1126
+ options: indexOptions,
1127
+ error: summarizeIndexError(error)
1128
+ }, emitEvents);
1037
1129
  resolve();
1038
1130
  });
1039
1131
  });
@@ -1150,6 +1242,134 @@ function resolveModelHooksFactory(definition) {
1150
1242
  const hooks = toCompatDefinition(definition).hooks;
1151
1243
  return typeof hooks === "function" ? hooks : null;
1152
1244
  }
1245
+ function collectModelIndexDefinitions(definition, softDeleteConfig) {
1246
+ const declared = [];
1247
+ if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1248
+ const key = { [softDeleteConfig.field]: 1 };
1249
+ const options = { expireAfterSeconds: softDeleteConfig.ttl };
1250
+ declared.push({
1251
+ source: "softDelete",
1252
+ key,
1253
+ options,
1254
+ name: getIndexOptionName(options),
1255
+ fingerprint: stableIndexStringify({ key, options })
1256
+ });
1257
+ }
1258
+ const indexes = toCompatDefinition(definition).indexes;
1259
+ if (!Array.isArray(indexes) || indexes.length === 0) {
1260
+ return declared;
1261
+ }
1262
+ for (const indexSpec of indexes) {
1263
+ if (!isRecord(indexSpec) || !indexSpec.key) {
1264
+ continue;
1265
+ }
1266
+ const { key, ...indexOptions } = indexSpec;
1267
+ declared.push({
1268
+ source: "definition",
1269
+ key,
1270
+ options: indexOptions,
1271
+ name: getIndexOptionName(indexOptions),
1272
+ fingerprint: stableIndexStringify({ key, options: indexOptions })
1273
+ });
1274
+ }
1275
+ return declared;
1276
+ }
1277
+ async function ensureModelIndexesForCollection(collection, definition, softDeleteConfig, options = {}) {
1278
+ const namespace = toIndexNamespace(resolveIndexTaskScope(collection, options));
1279
+ const declared = collectModelIndexDefinitions(definition, softDeleteConfig);
1280
+ const existingIndexes = await collection.listIndexes();
1281
+ const existing = [];
1282
+ const missing = [];
1283
+ const conflicts = [];
1284
+ for (const declaredIndex of declared) {
1285
+ const existingByName = findExistingIndexByName(existingIndexes, declaredIndex.name);
1286
+ if (existingByName) {
1287
+ if (indexOptionsMatch(existingByName, declaredIndex)) {
1288
+ existing.push({ declared: declaredIndex, existing: existingByName });
1289
+ } else {
1290
+ conflicts.push({
1291
+ declared: declaredIndex,
1292
+ existing: existingByName,
1293
+ reason: "name-conflict"
1294
+ });
1295
+ }
1296
+ continue;
1297
+ }
1298
+ const existingByKey = findExistingIndexByKey(existingIndexes, declaredIndex.key);
1299
+ if (existingByKey) {
1300
+ if (indexOptionsMatch(existingByKey, declaredIndex)) {
1301
+ existing.push({ declared: declaredIndex, existing: existingByKey });
1302
+ } else {
1303
+ conflicts.push({
1304
+ declared: declaredIndex,
1305
+ existing: existingByKey,
1306
+ reason: "options-conflict"
1307
+ });
1308
+ }
1309
+ continue;
1310
+ }
1311
+ missing.push(declaredIndex);
1312
+ }
1313
+ const result = {
1314
+ dryRun: options.dryRun === true,
1315
+ namespace,
1316
+ declared,
1317
+ existing,
1318
+ missing,
1319
+ created: [],
1320
+ conflicts,
1321
+ failed: [],
1322
+ skipped: options.dryRun === true ? missing.map((declaredIndex) => ({ declared: declaredIndex, reason: "dry-run" })) : conflicts.map((conflict) => ({ declared: conflict.declared, reason: conflict.reason }))
1323
+ };
1324
+ if (conflicts.length > 0 && options.throwOnError) {
1325
+ throw createIndexEnsureError("Model index conflicts detected.", result);
1326
+ }
1327
+ if (options.dryRun === true) {
1328
+ return result;
1329
+ }
1330
+ for (const declaredIndex of missing) {
1331
+ try {
1332
+ const createdName = await collection.createIndex(declaredIndex.key, declaredIndex.options);
1333
+ result.created.push({
1334
+ declared: declaredIndex,
1335
+ name: typeof createdName === "string" ? createdName : void 0,
1336
+ result: createdName
1337
+ });
1338
+ } catch (error) {
1339
+ result.failed.push({
1340
+ declared: declaredIndex,
1341
+ error: summarizeIndexError(error)
1342
+ });
1343
+ if (options.throwOnError) {
1344
+ throw createIndexEnsureError(
1345
+ "Model index creation failed.",
1346
+ result,
1347
+ error instanceof Error ? error : void 0
1348
+ );
1349
+ }
1350
+ }
1351
+ }
1352
+ return result;
1353
+ }
1354
+ function summarizeModelIndexEnsureResults(results) {
1355
+ return results.reduce((totals, result) => ({
1356
+ declared: totals.declared + result.declared.length,
1357
+ existing: totals.existing + result.existing.length,
1358
+ missing: totals.missing + result.missing.length,
1359
+ created: totals.created + result.created.length,
1360
+ conflicts: totals.conflicts + result.conflicts.length,
1361
+ failed: totals.failed + result.failed.length,
1362
+ skipped: totals.skipped + result.skipped.length
1363
+ }), {
1364
+ declared: 0,
1365
+ existing: 0,
1366
+ missing: 0,
1367
+ created: 0,
1368
+ conflicts: 0,
1369
+ failed: 0,
1370
+ skipped: 0
1371
+ });
1372
+ }
1153
1373
  function initializeModelV1Methods(target, definition) {
1154
1374
  const methods = toCompatDefinition(definition).methods;
1155
1375
  if (typeof methods !== "function") {
@@ -1174,25 +1394,12 @@ function initializeModelV1Methods(target, definition) {
1174
1394
  }
1175
1395
  }
1176
1396
  function scheduleModelIndexes(collection, definition, softDeleteConfig, options) {
1177
- if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1178
- const softDeleteIndex = softDeleteConfig;
1179
- scheduleIndexTask(
1180
- collection,
1181
- { [softDeleteIndex.field]: 1 },
1182
- { expireAfterSeconds: softDeleteIndex.ttl },
1183
- options
1184
- );
1185
- }
1186
- const indexes = toCompatDefinition(definition).indexes;
1187
- if (!Array.isArray(indexes) || indexes.length === 0) {
1397
+ const autoIndex = resolveModelAutoIndexOptions(definition, options?.autoIndex);
1398
+ if (!autoIndex.enabled) {
1188
1399
  return;
1189
1400
  }
1190
- for (const indexSpec of indexes) {
1191
- if (!indexSpec?.key) {
1192
- continue;
1193
- }
1194
- const { key, ...indexOptions } = indexSpec;
1195
- scheduleIndexTask(collection, key, indexOptions, options);
1401
+ for (const declaredIndex of collectModelIndexDefinitions(definition, softDeleteConfig)) {
1402
+ scheduleIndexTask(collection, declaredIndex, autoIndex.emitEvents, options);
1196
1403
  }
1197
1404
  }
1198
1405
 
@@ -1760,7 +1967,8 @@ var ModelInstance = class {
1760
1967
  runtime: this.runtime,
1761
1968
  dbName: options.dbName,
1762
1969
  poolName: options.poolName,
1763
- collectionName: options.collectionName
1970
+ collectionName: options.collectionName,
1971
+ autoIndex: this.runtime.options?.autoIndex
1764
1972
  });
1765
1973
  this._v1InstanceMethods = initializeModelV1Methods(this, options.definition);
1766
1974
  }
@@ -1964,6 +2172,15 @@ var ModelInstance = class {
1964
2172
  listIndexes() {
1965
2173
  return this.collection.listIndexes();
1966
2174
  }
2175
+ ensureIndexes(options = {}) {
2176
+ return ensureModelIndexesForCollection(this.collection, this.definition, this._softDeleteConfig, {
2177
+ ...options,
2178
+ runtime: this.runtime,
2179
+ dbName: this.dbName,
2180
+ poolName: this.poolName,
2181
+ collectionName: this.collectionName
2182
+ });
2183
+ }
1967
2184
  dropIndex(name) {
1968
2185
  return this.collection.dropIndex(name);
1969
2186
  }
@@ -8062,6 +8279,26 @@ function createRuntimeModelInstance(host, name, scope) {
8062
8279
  });
8063
8280
  return instance;
8064
8281
  }
8282
+ async function ensureRuntimeModelIndexes(host, options = {}) {
8283
+ const modelNames = options.models ?? Model.list();
8284
+ const models = [];
8285
+ for (const name of modelNames) {
8286
+ const model = host.scopedModel(name, {
8287
+ database: options.database,
8288
+ pool: options.pool
8289
+ });
8290
+ const result = await model.ensureIndexes({
8291
+ dryRun: options.dryRun,
8292
+ throwOnError: options.throwOnError
8293
+ });
8294
+ models.push({ name, result });
8295
+ }
8296
+ return {
8297
+ dryRun: options.dryRun === true,
8298
+ models,
8299
+ totals: summarizeModelIndexEnsureResults(models.map((item) => item.result))
8300
+ };
8301
+ }
8065
8302
 
8066
8303
  // src/entry/runtime-core-hosts.ts
8067
8304
  function resolveAdapterCache(state) {
@@ -10773,7 +11010,8 @@ var MonSQLizeRuntime = class {
10773
11010
  namespace: d.namespace,
10774
11011
  log: d.log,
10775
11012
  countQueue: this.options.countQueue,
10776
- models: this.options.models
11013
+ models: this.options.models,
11014
+ autoIndex: this.options.autoIndex
10777
11015
  };
10778
11016
  }
10779
11017
  async close() {
@@ -10970,6 +11208,10 @@ var MonSQLizeRuntime = class {
10970
11208
  cache.set(name, instance);
10971
11209
  return instance;
10972
11210
  }
11211
+ async ensureModelIndexes(options = {}) {
11212
+ this.ensureConnected();
11213
+ return ensureRuntimeModelIndexes(this, options);
11214
+ }
10973
11215
  // Capability delegation ----------------------------------------------------
10974
11216
  async startSession(options = {}) {
10975
11217
  this.ensureConnected();
@@ -62,6 +62,104 @@ export interface VirtualConfig {
62
62
  set?: (this: Record<string, unknown>, value: unknown) => void;
63
63
  }
64
64
 
65
+ export type ModelAutoIndexOptions = boolean | {
66
+ /** Enable automatic model index creation. Defaults to true for backward compatibility. */
67
+ enabled?: boolean;
68
+ /** Emit `model-index-error` when automatic index creation fails. Defaults to true. */
69
+ emitEvents?: boolean;
70
+ };
71
+
72
+ export type ModelIndexSource = 'definition' | 'softDelete';
73
+
74
+ export interface ModelDeclaredIndex {
75
+ source: ModelIndexSource;
76
+ key: unknown;
77
+ options: Record<string, unknown>;
78
+ name?: string;
79
+ fingerprint: string;
80
+ }
81
+
82
+ export interface ModelIndexNamespace {
83
+ db: string;
84
+ collection: string;
85
+ poolName: string;
86
+ }
87
+
88
+ export interface ModelIndexErrorSummary {
89
+ name?: string;
90
+ message: string;
91
+ code?: unknown;
92
+ }
93
+
94
+ export interface ModelIndexEnsureExisting {
95
+ declared: ModelDeclaredIndex;
96
+ existing: Record<string, unknown>;
97
+ }
98
+
99
+ export interface ModelIndexConflict {
100
+ declared: ModelDeclaredIndex;
101
+ existing?: Record<string, unknown>;
102
+ reason: string;
103
+ }
104
+
105
+ export interface ModelIndexCreated {
106
+ declared: ModelDeclaredIndex;
107
+ name?: string;
108
+ result?: unknown;
109
+ }
110
+
111
+ export interface ModelIndexFailure {
112
+ declared: ModelDeclaredIndex;
113
+ error: ModelIndexErrorSummary;
114
+ }
115
+
116
+ export interface ModelIndexSkipped {
117
+ declared: ModelDeclaredIndex;
118
+ reason: string;
119
+ }
120
+
121
+ export interface ModelEnsureIndexesOptions {
122
+ /** Return the index diff without creating missing indexes. */
123
+ dryRun?: boolean;
124
+ /** Throw a MonSQLize `MONGODB_ERROR` when conflicts or creation failures are found. */
125
+ throwOnError?: boolean;
126
+ }
127
+
128
+ export interface ModelEnsureAllIndexesOptions extends ModelEnsureIndexesOptions {
129
+ /** Limit the operation to specific registered model names. Defaults to all models. */
130
+ models?: string[];
131
+ /** Optional database scope for models without their own connection override. */
132
+ database?: string;
133
+ /** Optional pool scope for models without their own connection override. */
134
+ pool?: string;
135
+ }
136
+
137
+ export interface ModelIndexEnsureResult {
138
+ dryRun: boolean;
139
+ namespace: ModelIndexNamespace;
140
+ declared: ModelDeclaredIndex[];
141
+ existing: ModelIndexEnsureExisting[];
142
+ missing: ModelDeclaredIndex[];
143
+ created: ModelIndexCreated[];
144
+ conflicts: ModelIndexConflict[];
145
+ failed: ModelIndexFailure[];
146
+ skipped: ModelIndexSkipped[];
147
+ }
148
+
149
+ export interface ModelIndexEnsureSummary {
150
+ dryRun: boolean;
151
+ models: Array<{ name: string; result: ModelIndexEnsureResult }>;
152
+ totals: {
153
+ declared: number;
154
+ existing: number;
155
+ missing: number;
156
+ created: number;
157
+ conflicts: number;
158
+ failed: number;
159
+ skipped: number;
160
+ };
161
+ }
162
+
65
163
  /** v1 hooks factory format */
66
164
  export type V1HooksFactory<TDocument = Record<string, unknown>> = (
67
165
  model: ModelInstance<TDocument>,
@@ -95,6 +193,7 @@ export type V1MethodsFactory<TDocument = Record<string, unknown>> = (
95
193
  export interface ModelDefinitionOptions {
96
194
  timestamps?: boolean | { createdAt?: string | boolean; updatedAt?: string | boolean };
97
195
  validate?: boolean;
196
+ autoIndex?: ModelAutoIndexOptions;
98
197
  softDelete?: boolean | {
99
198
  enabled?: boolean;
100
199
  field?: string;
@@ -443,6 +542,11 @@ export interface ModelInstance<TDocument = any> {
443
542
  createIndexes(specs: Array<{ key: unknown; } & Record<string, unknown>>): Promise<string[]>;
444
543
  /** Lists all existing index definitions on the collection. */
445
544
  listIndexes(): Promise<Record<string, unknown>[]>;
545
+ /**
546
+ * Compares declared model indexes with the database and optionally creates missing indexes.
547
+ * Does not drop, rename, or rebuild conflicting indexes.
548
+ */
549
+ ensureIndexes(options?: ModelEnsureIndexesOptions): Promise<ModelIndexEnsureResult>;
446
550
  /**
447
551
  * Drops the specified index by name.
448
552
  * @param name Index name.
@@ -33,7 +33,7 @@ export interface SSHConfig {
33
33
 
34
34
  import type { Collection, DbAccessor, HealthView } from './collection';
35
35
  import type { Lock, LockOptions, LockStats } from './lock';
36
- import type { ModelInstance } from './model';
36
+ import type { ModelAutoIndexOptions, ModelEnsureAllIndexesOptions, ModelIndexEnsureSummary, ModelInstance } from './model';
37
37
  import type { MongoConnectConfig } from './mongodb';
38
38
  import type { ConnectionPoolManagerOptions, PoolConfig, PoolHealthStatus, PoolStats, PoolStrategy } from './pool';
39
39
  import type {
@@ -176,6 +176,8 @@ export interface MonSQLizeOptions {
176
176
  countQueue?: boolean | { enabled?: boolean; concurrency?: number; maxQueueSize?: number; timeout?: number; };
177
177
  /** Model definitions to auto-register on connect. Accepts a file path (string) or an object with { path, pattern?, recursive? }. @since v1.3.0 */
178
178
  models?: string | { path: string; pattern?: string; recursive?: boolean; };
179
+ /** Global automatic model index creation control. Defaults to true for backward compatibility. */
180
+ autoIndex?: ModelAutoIndexOptions;
179
181
  /** Auto-invalidate cache on write operations. @since v1.3.0 */
180
182
  cacheAutoInvalidate?: boolean;
181
183
  }
@@ -256,6 +258,11 @@ export interface MonSQLizeInstance {
256
258
  */
257
259
  model<TDocument = any>(name: string): ModelInstance<TDocument>;
258
260
  model(name: string): ModelInstance<any>;
261
+ /**
262
+ * Ensures declared indexes for registered models.
263
+ * Use `dryRun: true` for production preflight; execution only creates missing indexes.
264
+ */
265
+ ensureModelIndexes(options?: ModelEnsureAllIndexesOptions): Promise<ModelIndexEnsureSummary>;
259
266
  /**
260
267
  * Start a MongoDB transaction session.
261
268
  * @param options Optional transaction options.
@@ -442,6 +449,7 @@ export default class MonSQLize implements MonSQLizeInstance {
442
449
  scopedModel(name: string, options?: { database?: string; pool?: string; }): ModelInstance<any>;
443
450
  model<TDocument = any>(name: string): ModelInstance<TDocument>;
444
451
  model(name: string): ModelInstance<any>;
452
+ ensureModelIndexes(options?: ModelEnsureAllIndexesOptions): Promise<ModelIndexEnsureSummary>;
445
453
  startSession(options?: TransactionOptions): Promise<Transaction>;
446
454
  withTransaction<T>(callback: (transaction: Transaction) => Promise<T>, options?: TransactionOptions): Promise<T>;
447
455
  withLock<T>(key: string, callback: () => Promise<T>, options?: LockOptions): Promise<T>;
@@ -1,6 +1,6 @@
1
1
  import type { BookmarkClearResult, BookmarkListResult, BookmarkPrewarmResult, DeleteBatchResult, DeleteResult, IncrementOneResult, IndexCreateResult, InsertBatchResult, InsertManyResult, UpdateBatchResult, UpdateResult } from './collection';
2
2
  import type { LoggerLike, ExpressionFunction, ExpressionObject } from './base';
3
- import type { ModelDefinition, ModelInstance as ModelInstanceContract, RegisteredModel, RelationConfig } from './model';
3
+ import type { ModelDefinition, ModelEnsureIndexesOptions, ModelIndexEnsureResult, ModelInstance as ModelInstanceContract, RegisteredModel, RelationConfig } from './model';
4
4
  import type { LockOptions, LockStats } from './lock';
5
5
  import type { ConnectionPoolManagerOptions, FallbackStrategy, PoolConfig, PoolHealthStatus, PoolRole, PoolStats, PoolStrategy } from './pool';
6
6
  import type { SagaDefinition, SagaOrchestratorOptions, SagaResult, SagaStats, SagaStep } from './saga';
@@ -335,6 +335,7 @@ export declare class ModelInstance<TDocument = any> implements ModelInstanceCont
335
335
  createIndex(keys: unknown, options?: unknown): Promise<IndexCreateResult>;
336
336
  createIndexes(specs: Array<{ key: unknown; } & Record<string, unknown>>): Promise<string[]>;
337
337
  listIndexes(): Promise<Record<string, unknown>[]>;
338
+ ensureIndexes(options?: ModelEnsureIndexesOptions): Promise<ModelIndexEnsureResult>;
338
339
  dropIndex(name: string): Promise<unknown>;
339
340
  dropIndexes(): Promise<unknown>;
340
341
  prewarmBookmarks(keyDims?: unknown, pages?: number[]): Promise<BookmarkPrewarmResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monsqlize",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "TypeScript-native MongoDB ODM with multi-level caching (cache-hub), distributed locks, Saga orchestration, unified expression system (122 operators), connection pool management, ChangeStream sync, slow-query logging, and full v1 API compatibility",
5
5
  "type": "commonjs",
6
6
  "main": "./dist/cjs/index.cjs",
@@ -18,6 +18,7 @@
18
18
  "dist/**/*.cjs",
19
19
  "dist/**/*.mjs",
20
20
  "dist/**/*.d.ts",
21
+ "changelogs/v2.0.4.md",
21
22
  "changelogs/v2.0.3.md",
22
23
  "changelogs/v2.0.2.md",
23
24
  "changelogs/v2.0.1.md",
@@ -115,7 +116,7 @@
115
116
  "cache-hub": "2.2.4",
116
117
  "ioredis": "5.8.2",
117
118
  "mongodb": "6.21.0",
118
- "schema-dsl": "2.0.8",
119
+ "schema-dsl": "2.0.9",
119
120
  "ssh2": "1.17.0"
120
121
  }
121
122
  }